diff --git a/backend/app/DomainObjects/AttendeeDomainObject.php b/backend/app/DomainObjects/AttendeeDomainObject.php index c8101dd7..ee9ceeb9 100644 --- a/backend/app/DomainObjects/AttendeeDomainObject.php +++ b/backend/app/DomainObjects/AttendeeDomainObject.php @@ -11,7 +11,7 @@ class AttendeeDomainObject extends Generated\AttendeeDomainObjectAbstract implem { private ?OrderDomainObject $order = null; - private ?TicketDomainObject $ticket = null; + private ?ProductDomainObject $product = null; /** @var Collection|null */ public ?Collection $questionAndAnswerViews = null; @@ -60,7 +60,7 @@ public static function getAllowedFilterFields(): array { return [ self::STATUS, - self::TICKET_ID, + self::PRODUCT_ID, ]; } @@ -79,14 +79,14 @@ public function getFullName(): string return $this->first_name . ' ' . $this->last_name; } - public function getTicket(): ?TicketDomainObject + public function getProduct(): ?ProductDomainObject { - return $this->ticket; + return $this->product; } - public function setTicket(?TicketDomainObject $ticket): self + public function setProduct(?ProductDomainObject $product): self { - $this->ticket = $ticket; + $this->product = $product; return $this; } diff --git a/backend/app/DomainObjects/CapacityAssignmentDomainObject.php b/backend/app/DomainObjects/CapacityAssignmentDomainObject.php index 758847d2..642fadae 100644 --- a/backend/app/DomainObjects/CapacityAssignmentDomainObject.php +++ b/backend/app/DomainObjects/CapacityAssignmentDomainObject.php @@ -9,7 +9,7 @@ class CapacityAssignmentDomainObject extends Generated\CapacityAssignmentDomainObjectAbstract implements IsSortable { - public ?Collection $tickets = null; + public ?Collection $products = null; public static function getDefaultSort(): string { @@ -58,14 +58,14 @@ public function getPercentageUsed(): float return round(($this->getUsedCapacity() / $this->getCapacity()) * 100, 2); } - public function getTickets(): ?Collection + public function getProducts(): ?Collection { - return $this->tickets; + return $this->products; } - public function setTickets(?Collection $tickets): static + public function setProducts(?Collection $products): static { - $this->tickets = $tickets; + $this->products = $products; return $this; } diff --git a/backend/app/DomainObjects/CheckInListDomainObject.php b/backend/app/DomainObjects/CheckInListDomainObject.php index 30364989..ae55f3bb 100644 --- a/backend/app/DomainObjects/CheckInListDomainObject.php +++ b/backend/app/DomainObjects/CheckInListDomainObject.php @@ -9,7 +9,7 @@ class CheckInListDomainObject extends Generated\CheckInListDomainObjectAbstract implements IsSortable { - private ?Collection $tickets = null; + private ?Collection $products = null; private ?EventDomainObject $event = null; @@ -53,14 +53,14 @@ public static function getAllowedSorts(): AllowedSorts ); } - public function getTickets(): ?Collection + public function getProducts(): ?Collection { - return $this->tickets; + return $this->products; } - public function setTickets(?Collection $tickets): static + public function setProducts(?Collection $products): static { - $this->tickets = $tickets; + $this->products = $products; return $this; } diff --git a/backend/app/DomainObjects/Enums/CapacityAssignmentAppliesTo.php b/backend/app/DomainObjects/Enums/CapacityAssignmentAppliesTo.php index 75d27759..250b77a3 100644 --- a/backend/app/DomainObjects/Enums/CapacityAssignmentAppliesTo.php +++ b/backend/app/DomainObjects/Enums/CapacityAssignmentAppliesTo.php @@ -6,6 +6,6 @@ enum CapacityAssignmentAppliesTo { use BaseEnum; - case TICKETS; + case PRODUCTS; case EVENT; } diff --git a/backend/app/DomainObjects/Enums/MessageTypeEnum.php b/backend/app/DomainObjects/Enums/MessageTypeEnum.php index 4246d750..fd2b44b1 100644 --- a/backend/app/DomainObjects/Enums/MessageTypeEnum.php +++ b/backend/app/DomainObjects/Enums/MessageTypeEnum.php @@ -7,7 +7,7 @@ enum MessageTypeEnum use BaseEnum; case ORDER; - case TICKET; + case PRODUCT; case ATTENDEE; case EVENT; } diff --git a/backend/app/DomainObjects/Enums/TicketType.php b/backend/app/DomainObjects/Enums/ProductPriceType.php similarity index 87% rename from backend/app/DomainObjects/Enums/TicketType.php rename to backend/app/DomainObjects/Enums/ProductPriceType.php index 942a4515..a0d01c44 100644 --- a/backend/app/DomainObjects/Enums/TicketType.php +++ b/backend/app/DomainObjects/Enums/ProductPriceType.php @@ -2,7 +2,7 @@ namespace HiEvents\DomainObjects\Enums; -enum TicketType +enum ProductPriceType { use BaseEnum; diff --git a/backend/app/DomainObjects/Enums/ProductType.php b/backend/app/DomainObjects/Enums/ProductType.php new file mode 100644 index 00000000..03a9b5b5 --- /dev/null +++ b/backend/app/DomainObjects/Enums/ProductType.php @@ -0,0 +1,11 @@ +tickets = $tickets; + $this->products = $products; return $this; } - public function getTickets(): ?Collection + public function getProducts(): ?Collection { - return $this->tickets; + return $this->products; } public function setQuestions(?Collection $questions): EventDomainObject @@ -259,4 +261,15 @@ public function setEventStatistics(?EventStatisticDomainObject $eventStatistics) $this->eventStatistics = $eventStatistics; return $this; } + + public function setProductCategories(?Collection $productCategories): EventDomainObject + { + $this->productCategories = $productCategories; + return $this; + } + + public function getProductCategories(): ?Collection + { + return $this->productCategories; + } } diff --git a/backend/app/DomainObjects/Generated/AttendeeCheckInDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/AttendeeCheckInDomainObjectAbstract.php index ffb56a8d..1249d271 100644 --- a/backend/app/DomainObjects/Generated/AttendeeCheckInDomainObjectAbstract.php +++ b/backend/app/DomainObjects/Generated/AttendeeCheckInDomainObjectAbstract.php @@ -12,7 +12,7 @@ abstract class AttendeeCheckInDomainObjectAbstract extends \HiEvents\DomainObjec final public const PLURAL_NAME = 'attendee_check_ins'; final public const ID = 'id'; final public const CHECK_IN_LIST_ID = 'check_in_list_id'; - final public const TICKET_ID = 'ticket_id'; + final public const PRODUCT_ID = 'product_id'; final public const ATTENDEE_ID = 'attendee_id'; final public const EVENT_ID = 'event_id'; final public const SHORT_ID = 'short_id'; @@ -23,7 +23,7 @@ abstract class AttendeeCheckInDomainObjectAbstract extends \HiEvents\DomainObjec protected int $id; protected int $check_in_list_id; - protected int $ticket_id; + protected int $product_id; protected int $attendee_id; protected int $event_id; protected string $short_id; @@ -37,7 +37,7 @@ public function toArray(): array return [ 'id' => $this->id ?? null, 'check_in_list_id' => $this->check_in_list_id ?? null, - 'ticket_id' => $this->ticket_id ?? null, + 'product_id' => $this->product_id ?? null, 'attendee_id' => $this->attendee_id ?? null, 'event_id' => $this->event_id ?? null, 'short_id' => $this->short_id ?? null, @@ -70,15 +70,15 @@ public function getCheckInListId(): int return $this->check_in_list_id; } - public function setTicketId(int $ticket_id): self + public function setProductId(int $product_id): self { - $this->ticket_id = $ticket_id; + $this->product_id = $product_id; return $this; } - public function getTicketId(): int + public function getProductId(): int { - return $this->ticket_id; + return $this->product_id; } public function setAttendeeId(int $attendee_id): self diff --git a/backend/app/DomainObjects/Generated/AttendeeDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/AttendeeDomainObjectAbstract.php index 59b052de..be3ca97e 100644 --- a/backend/app/DomainObjects/Generated/AttendeeDomainObjectAbstract.php +++ b/backend/app/DomainObjects/Generated/AttendeeDomainObjectAbstract.php @@ -12,11 +12,11 @@ abstract class AttendeeDomainObjectAbstract extends \HiEvents\DomainObjects\Abst final public const PLURAL_NAME = 'attendees'; final public const ID = 'id'; final public const ORDER_ID = 'order_id'; - final public const TICKET_ID = 'ticket_id'; + final public const PRODUCT_ID = 'product_id'; final public const EVENT_ID = 'event_id'; final public const CHECKED_IN_BY = 'checked_in_by'; final public const CHECKED_OUT_BY = 'checked_out_by'; - final public const TICKET_PRICE_ID = 'ticket_price_id'; + final public const PRODUCT_PRICE_ID = 'product_price_id'; final public const SHORT_ID = 'short_id'; final public const FIRST_NAME = 'first_name'; final public const LAST_NAME = 'last_name'; @@ -28,14 +28,15 @@ abstract class AttendeeDomainObjectAbstract extends \HiEvents\DomainObjects\Abst final public const UPDATED_AT = 'updated_at'; final public const DELETED_AT = 'deleted_at'; final public const LOCALE = 'locale'; + final public const NOTES = 'notes'; protected int $id; protected int $order_id; - protected int $ticket_id; + protected int $product_id; protected int $event_id; protected ?int $checked_in_by = null; protected ?int $checked_out_by = null; - protected int $ticket_price_id; + protected int $product_price_id; protected string $short_id; protected string $first_name = ''; protected string $last_name = ''; @@ -47,17 +48,18 @@ abstract class AttendeeDomainObjectAbstract extends \HiEvents\DomainObjects\Abst protected string $updated_at; protected ?string $deleted_at = null; protected string $locale = 'en'; + protected ?string $notes = null; public function toArray(): array { return [ 'id' => $this->id ?? null, 'order_id' => $this->order_id ?? null, - 'ticket_id' => $this->ticket_id ?? null, + 'product_id' => $this->product_id ?? null, 'event_id' => $this->event_id ?? null, 'checked_in_by' => $this->checked_in_by ?? null, 'checked_out_by' => $this->checked_out_by ?? null, - 'ticket_price_id' => $this->ticket_price_id ?? null, + 'product_price_id' => $this->product_price_id ?? null, 'short_id' => $this->short_id ?? null, 'first_name' => $this->first_name ?? null, 'last_name' => $this->last_name ?? null, @@ -69,6 +71,7 @@ public function toArray(): array 'updated_at' => $this->updated_at ?? null, 'deleted_at' => $this->deleted_at ?? null, 'locale' => $this->locale ?? null, + 'notes' => $this->notes ?? null, ]; } @@ -94,15 +97,15 @@ public function getOrderId(): int return $this->order_id; } - public function setTicketId(int $ticket_id): self + public function setProductId(int $product_id): self { - $this->ticket_id = $ticket_id; + $this->product_id = $product_id; return $this; } - public function getTicketId(): int + public function getProductId(): int { - return $this->ticket_id; + return $this->product_id; } public function setEventId(int $event_id): self @@ -138,15 +141,15 @@ public function getCheckedOutBy(): ?int return $this->checked_out_by; } - public function setTicketPriceId(int $ticket_price_id): self + public function setProductPriceId(int $product_price_id): self { - $this->ticket_price_id = $ticket_price_id; + $this->product_price_id = $product_price_id; return $this; } - public function getTicketPriceId(): int + public function getProductPriceId(): int { - return $this->ticket_price_id; + return $this->product_price_id; } public function setShortId(string $short_id): self @@ -269,4 +272,15 @@ public function getLocale(): string { return $this->locale; } + + public function setNotes(?string $notes): self + { + $this->notes = $notes; + return $this; + } + + public function getNotes(): ?string + { + return $this->notes; + } } diff --git a/backend/app/DomainObjects/Generated/EventDailyStatisticDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/EventDailyStatisticDomainObjectAbstract.php index beae7bfa..ac1749cb 100644 --- a/backend/app/DomainObjects/Generated/EventDailyStatisticDomainObjectAbstract.php +++ b/backend/app/DomainObjects/Generated/EventDailyStatisticDomainObjectAbstract.php @@ -15,7 +15,7 @@ abstract class EventDailyStatisticDomainObjectAbstract extends \HiEvents\DomainO final public const SALES_TOTAL_GROSS = 'sales_total_gross'; final public const TOTAL_TAX = 'total_tax'; final public const SALES_TOTAL_BEFORE_ADDITIONS = 'sales_total_before_additions'; - final public const TICKETS_SOLD = 'tickets_sold'; + final public const PRODUCTS_SOLD = 'products_sold'; final public const ORDERS_CREATED = 'orders_created'; final public const DATE = 'date'; final public const CREATED_AT = 'created_at'; @@ -25,13 +25,14 @@ abstract class EventDailyStatisticDomainObjectAbstract extends \HiEvents\DomainO final public const VERSION = 'version'; final public const TOTAL_REFUNDED = 'total_refunded'; final public const TOTAL_VIEWS = 'total_views'; + final public const ATTENDEES_REGISTERED = 'attendees_registered'; protected int $id; protected int $event_id; protected float $sales_total_gross = 0.0; protected float $total_tax = 0.0; protected float $sales_total_before_additions = 0.0; - protected int $tickets_sold = 0; + protected int $products_sold = 0; protected int $orders_created = 0; protected string $date; protected string $created_at; @@ -41,6 +42,7 @@ abstract class EventDailyStatisticDomainObjectAbstract extends \HiEvents\DomainO protected int $version = 0; protected float $total_refunded = 0.0; protected int $total_views = 0; + protected int $attendees_registered = 0; public function toArray(): array { @@ -50,7 +52,7 @@ public function toArray(): array 'sales_total_gross' => $this->sales_total_gross ?? null, 'total_tax' => $this->total_tax ?? null, 'sales_total_before_additions' => $this->sales_total_before_additions ?? null, - 'tickets_sold' => $this->tickets_sold ?? null, + 'products_sold' => $this->products_sold ?? null, 'orders_created' => $this->orders_created ?? null, 'date' => $this->date ?? null, 'created_at' => $this->created_at ?? null, @@ -60,6 +62,7 @@ public function toArray(): array 'version' => $this->version ?? null, 'total_refunded' => $this->total_refunded ?? null, 'total_views' => $this->total_views ?? null, + 'attendees_registered' => $this->attendees_registered ?? null, ]; } @@ -118,15 +121,15 @@ public function getSalesTotalBeforeAdditions(): float return $this->sales_total_before_additions; } - public function setTicketsSold(int $tickets_sold): self + public function setProductsSold(int $products_sold): self { - $this->tickets_sold = $tickets_sold; + $this->products_sold = $products_sold; return $this; } - public function getTicketsSold(): int + public function getProductsSold(): int { - return $this->tickets_sold; + return $this->products_sold; } public function setOrdersCreated(int $orders_created): self @@ -227,4 +230,15 @@ public function getTotalViews(): int { return $this->total_views; } + + public function setAttendeesRegistered(int $attendees_registered): self + { + $this->attendees_registered = $attendees_registered; + return $this; + } + + public function getAttendeesRegistered(): int + { + return $this->attendees_registered; + } } diff --git a/backend/app/DomainObjects/Generated/EventSettingDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/EventSettingDomainObjectAbstract.php index a883a7e2..03feec38 100644 --- a/backend/app/DomainObjects/Generated/EventSettingDomainObjectAbstract.php +++ b/backend/app/DomainObjects/Generated/EventSettingDomainObjectAbstract.php @@ -14,7 +14,7 @@ abstract class EventSettingDomainObjectAbstract extends \HiEvents\DomainObjects\ final public const EVENT_ID = 'event_id'; final public const PRE_CHECKOUT_MESSAGE = 'pre_checkout_message'; final public const POST_CHECKOUT_MESSAGE = 'post_checkout_message'; - final public const TICKET_PAGE_MESSAGE = 'ticket_page_message'; + final public const PRODUCT_PAGE_MESSAGE = 'product_page_message'; final public const CONTINUE_BUTTON_TEXT = 'continue_button_text'; final public const EMAIL_FOOTER_MESSAGE = 'email_footer_message'; final public const SUPPORT_EMAIL = 'support_email'; @@ -50,7 +50,7 @@ abstract class EventSettingDomainObjectAbstract extends \HiEvents\DomainObjects\ protected int $event_id; protected ?string $pre_checkout_message = null; protected ?string $post_checkout_message = null; - protected ?string $ticket_page_message = null; + protected ?string $product_page_message = null; protected ?string $continue_button_text = null; protected ?string $email_footer_message = null; protected ?string $support_email = null; @@ -89,7 +89,7 @@ public function toArray(): array 'event_id' => $this->event_id ?? null, 'pre_checkout_message' => $this->pre_checkout_message ?? null, 'post_checkout_message' => $this->post_checkout_message ?? null, - 'ticket_page_message' => $this->ticket_page_message ?? null, + 'product_page_message' => $this->product_page_message ?? null, 'continue_button_text' => $this->continue_button_text ?? null, 'email_footer_message' => $this->email_footer_message ?? null, 'support_email' => $this->support_email ?? null, @@ -167,15 +167,15 @@ public function getPostCheckoutMessage(): ?string return $this->post_checkout_message; } - public function setTicketPageMessage(?string $ticket_page_message): self + public function setProductPageMessage(?string $product_page_message): self { - $this->ticket_page_message = $ticket_page_message; + $this->product_page_message = $product_page_message; return $this; } - public function getTicketPageMessage(): ?string + public function getProductPageMessage(): ?string { - return $this->ticket_page_message; + return $this->product_page_message; } public function setContinueButtonText(?string $continue_button_text): self diff --git a/backend/app/DomainObjects/Generated/EventStatisticDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/EventStatisticDomainObjectAbstract.php index 1519b8fb..e92c4640 100644 --- a/backend/app/DomainObjects/Generated/EventStatisticDomainObjectAbstract.php +++ b/backend/app/DomainObjects/Generated/EventStatisticDomainObjectAbstract.php @@ -21,10 +21,11 @@ abstract class EventStatisticDomainObjectAbstract extends \HiEvents\DomainObject final public const DELETED_AT = 'deleted_at'; final public const UPDATED_AT = 'updated_at'; final public const TOTAL_FEE = 'total_fee'; - final public const TICKETS_SOLD = 'tickets_sold'; + final public const PRODUCTS_SOLD = 'products_sold'; final public const VERSION = 'version'; final public const ORDERS_CREATED = 'orders_created'; final public const TOTAL_REFUNDED = 'total_refunded'; + final public const ATTENDEES_REGISTERED = 'attendees_registered'; protected int $id; protected int $event_id; @@ -37,10 +38,11 @@ abstract class EventStatisticDomainObjectAbstract extends \HiEvents\DomainObject protected ?string $deleted_at = null; protected ?string $updated_at = null; protected float $total_fee = 0.0; - protected int $tickets_sold = 0; + protected int $products_sold = 0; protected int $version = 0; protected int $orders_created = 0; protected float $total_refunded = 0.0; + protected int $attendees_registered = 0; public function toArray(): array { @@ -56,10 +58,11 @@ public function toArray(): array 'deleted_at' => $this->deleted_at ?? null, 'updated_at' => $this->updated_at ?? null, 'total_fee' => $this->total_fee ?? null, - 'tickets_sold' => $this->tickets_sold ?? null, + 'products_sold' => $this->products_sold ?? null, 'version' => $this->version ?? null, 'orders_created' => $this->orders_created ?? null, 'total_refunded' => $this->total_refunded ?? null, + 'attendees_registered' => $this->attendees_registered ?? null, ]; } @@ -184,15 +187,15 @@ public function getTotalFee(): float return $this->total_fee; } - public function setTicketsSold(int $tickets_sold): self + public function setProductsSold(int $products_sold): self { - $this->tickets_sold = $tickets_sold; + $this->products_sold = $products_sold; return $this; } - public function getTicketsSold(): int + public function getProductsSold(): int { - return $this->tickets_sold; + return $this->products_sold; } public function setVersion(int $version): self @@ -227,4 +230,15 @@ public function getTotalRefunded(): float { return $this->total_refunded; } + + public function setAttendeesRegistered(int $attendees_registered): self + { + $this->attendees_registered = $attendees_registered; + return $this; + } + + public function getAttendeesRegistered(): int + { + return $this->attendees_registered; + } } diff --git a/backend/app/DomainObjects/Generated/MessageDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/MessageDomainObjectAbstract.php index c496d4ac..acb847ef 100644 --- a/backend/app/DomainObjects/Generated/MessageDomainObjectAbstract.php +++ b/backend/app/DomainObjects/Generated/MessageDomainObjectAbstract.php @@ -19,7 +19,7 @@ abstract class MessageDomainObjectAbstract extends \HiEvents\DomainObjects\Abstr final public const RECIPIENT_IDS = 'recipient_ids'; final public const SENT_AT = 'sent_at'; final public const ATTENDEE_IDS = 'attendee_ids'; - final public const TICKET_IDS = 'ticket_ids'; + final public const PRODUCT_IDS = 'product_ids'; final public const ORDER_ID = 'order_id'; final public const STATUS = 'status'; final public const SEND_DATA = 'send_data'; @@ -36,7 +36,7 @@ abstract class MessageDomainObjectAbstract extends \HiEvents\DomainObjects\Abstr protected array|string|null $recipient_ids = null; protected ?string $sent_at = null; protected array|string|null $attendee_ids = null; - protected array|string|null $ticket_ids = null; + protected array|string|null $product_ids = null; protected ?int $order_id = null; protected string $status; protected array|string|null $send_data = null; @@ -56,7 +56,7 @@ public function toArray(): array 'recipient_ids' => $this->recipient_ids ?? null, 'sent_at' => $this->sent_at ?? null, 'attendee_ids' => $this->attendee_ids ?? null, - 'ticket_ids' => $this->ticket_ids ?? null, + 'product_ids' => $this->product_ids ?? null, 'order_id' => $this->order_id ?? null, 'status' => $this->status ?? null, 'send_data' => $this->send_data ?? null, @@ -165,15 +165,15 @@ public function getAttendeeIds(): array|string|null return $this->attendee_ids; } - public function setTicketIds(array|string|null $ticket_ids): self + public function setProductIds(array|string|null $product_ids): self { - $this->ticket_ids = $ticket_ids; + $this->product_ids = $product_ids; return $this; } - public function getTicketIds(): array|string|null + public function getProductIds(): array|string|null { - return $this->ticket_ids; + return $this->product_ids; } public function setOrderId(?int $order_id): self diff --git a/backend/app/DomainObjects/Generated/OrderItemDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/OrderItemDomainObjectAbstract.php index 71936c19..076da895 100644 --- a/backend/app/DomainObjects/Generated/OrderItemDomainObjectAbstract.php +++ b/backend/app/DomainObjects/Generated/OrderItemDomainObjectAbstract.php @@ -12,8 +12,8 @@ abstract class OrderItemDomainObjectAbstract extends \HiEvents\DomainObjects\Abs final public const PLURAL_NAME = 'order_items'; final public const ID = 'id'; final public const ORDER_ID = 'order_id'; - final public const TICKET_ID = 'ticket_id'; - final public const TICKET_PRICE_ID = 'ticket_price_id'; + final public const PRODUCT_ID = 'product_id'; + final public const PRODUCT_PRICE_ID = 'product_price_id'; final public const TOTAL_BEFORE_ADDITIONS = 'total_before_additions'; final public const QUANTITY = 'quantity'; final public const ITEM_NAME = 'item_name'; @@ -24,11 +24,12 @@ abstract class OrderItemDomainObjectAbstract extends \HiEvents\DomainObjects\Abs final public const TOTAL_GROSS = 'total_gross'; final public const TOTAL_SERVICE_FEE = 'total_service_fee'; final public const TAXES_AND_FEES_ROLLUP = 'taxes_and_fees_rollup'; + final public const PRODUCT_TYPE = 'product_type'; protected int $id; protected int $order_id; - protected int $ticket_id; - protected int $ticket_price_id; + protected int $product_id; + protected int $product_price_id; protected float $total_before_additions; protected int $quantity; protected ?string $item_name = null; @@ -39,14 +40,15 @@ abstract class OrderItemDomainObjectAbstract extends \HiEvents\DomainObjects\Abs protected ?float $total_gross = null; protected ?float $total_service_fee = 0.0; protected array|string|null $taxes_and_fees_rollup = null; + protected string $product_type = 'TICKET'; public function toArray(): array { return [ 'id' => $this->id ?? null, 'order_id' => $this->order_id ?? null, - 'ticket_id' => $this->ticket_id ?? null, - 'ticket_price_id' => $this->ticket_price_id ?? null, + 'product_id' => $this->product_id ?? null, + 'product_price_id' => $this->product_price_id ?? null, 'total_before_additions' => $this->total_before_additions ?? null, 'quantity' => $this->quantity ?? null, 'item_name' => $this->item_name ?? null, @@ -57,6 +59,7 @@ public function toArray(): array 'total_gross' => $this->total_gross ?? null, 'total_service_fee' => $this->total_service_fee ?? null, 'taxes_and_fees_rollup' => $this->taxes_and_fees_rollup ?? null, + 'product_type' => $this->product_type ?? null, ]; } @@ -82,26 +85,26 @@ public function getOrderId(): int return $this->order_id; } - public function setTicketId(int $ticket_id): self + public function setProductId(int $product_id): self { - $this->ticket_id = $ticket_id; + $this->product_id = $product_id; return $this; } - public function getTicketId(): int + public function getProductId(): int { - return $this->ticket_id; + return $this->product_id; } - public function setTicketPriceId(int $ticket_price_id): self + public function setProductPriceId(int $product_price_id): self { - $this->ticket_price_id = $ticket_price_id; + $this->product_price_id = $product_price_id; return $this; } - public function getTicketPriceId(): int + public function getProductPriceId(): int { - return $this->ticket_price_id; + return $this->product_price_id; } public function setTotalBeforeAdditions(float $total_before_additions): self @@ -213,4 +216,15 @@ public function getTaxesAndFeesRollup(): array|string|null { return $this->taxes_and_fees_rollup; } + + public function setProductType(string $product_type): self + { + $this->product_type = $product_type; + return $this; + } + + public function getProductType(): string + { + return $this->product_type; + } } diff --git a/backend/app/DomainObjects/Generated/TicketCapacityAssignmentDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/ProductCapacityAssignmentDomainObjectAbstract.php similarity index 79% rename from backend/app/DomainObjects/Generated/TicketCapacityAssignmentDomainObjectAbstract.php rename to backend/app/DomainObjects/Generated/ProductCapacityAssignmentDomainObjectAbstract.php index 40ed8593..048264f4 100644 --- a/backend/app/DomainObjects/Generated/TicketCapacityAssignmentDomainObjectAbstract.php +++ b/backend/app/DomainObjects/Generated/ProductCapacityAssignmentDomainObjectAbstract.php @@ -6,19 +6,19 @@ * THIS FILE IS AUTOGENERATED - DO NOT EDIT IT DIRECTLY. * @package HiEvents\DomainObjects\Generated */ -abstract class TicketCapacityAssignmentDomainObjectAbstract extends \HiEvents\DomainObjects\AbstractDomainObject +abstract class ProductCapacityAssignmentDomainObjectAbstract extends \HiEvents\DomainObjects\AbstractDomainObject { - final public const SINGULAR_NAME = 'ticket_capacity_assignment'; - final public const PLURAL_NAME = 'ticket_capacity_assignments'; + final public const SINGULAR_NAME = 'product_capacity_assignment'; + final public const PLURAL_NAME = 'product_capacity_assignments'; final public const ID = 'id'; - final public const TICKET_ID = 'ticket_id'; + final public const PRODUCT_ID = 'product_id'; final public const CAPACITY_ASSIGNMENT_ID = 'capacity_assignment_id'; final public const CREATED_AT = 'created_at'; final public const UPDATED_AT = 'updated_at'; final public const DELETED_AT = 'deleted_at'; protected int $id; - protected int $ticket_id; + protected int $product_id; protected int $capacity_assignment_id; protected ?string $created_at = null; protected ?string $updated_at = null; @@ -28,7 +28,7 @@ public function toArray(): array { return [ 'id' => $this->id ?? null, - 'ticket_id' => $this->ticket_id ?? null, + 'product_id' => $this->product_id ?? null, 'capacity_assignment_id' => $this->capacity_assignment_id ?? null, 'created_at' => $this->created_at ?? null, 'updated_at' => $this->updated_at ?? null, @@ -47,15 +47,15 @@ public function getId(): int return $this->id; } - public function setTicketId(int $ticket_id): self + public function setProductId(int $product_id): self { - $this->ticket_id = $ticket_id; + $this->product_id = $product_id; return $this; } - public function getTicketId(): int + public function getProductId(): int { - return $this->ticket_id; + return $this->product_id; } public function setCapacityAssignmentId(int $capacity_assignment_id): self diff --git a/backend/app/DomainObjects/Generated/ProductCategoryDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/ProductCategoryDomainObjectAbstract.php new file mode 100644 index 00000000..19bc308d --- /dev/null +++ b/backend/app/DomainObjects/Generated/ProductCategoryDomainObjectAbstract.php @@ -0,0 +1,160 @@ + $this->id ?? null, + 'event_id' => $this->event_id ?? null, + 'name' => $this->name ?? null, + 'no_products_message' => $this->no_products_message ?? null, + 'description' => $this->description ?? null, + 'is_hidden' => $this->is_hidden ?? null, + 'order' => $this->order ?? null, + 'created_at' => $this->created_at ?? null, + 'updated_at' => $this->updated_at ?? null, + 'deleted_at' => $this->deleted_at ?? null, + ]; + } + + public function setId(int $id): self + { + $this->id = $id; + return $this; + } + + public function getId(): int + { + return $this->id; + } + + public function setEventId(int $event_id): self + { + $this->event_id = $event_id; + return $this; + } + + public function getEventId(): int + { + return $this->event_id; + } + + public function setName(string $name): self + { + $this->name = $name; + return $this; + } + + public function getName(): string + { + return $this->name; + } + + public function setNoProductsMessage(?string $no_products_message): self + { + $this->no_products_message = $no_products_message; + return $this; + } + + public function getNoProductsMessage(): ?string + { + return $this->no_products_message; + } + + public function setDescription(?string $description): self + { + $this->description = $description; + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setIsHidden(bool $is_hidden): self + { + $this->is_hidden = $is_hidden; + return $this; + } + + public function getIsHidden(): bool + { + return $this->is_hidden; + } + + public function setOrder(int $order): self + { + $this->order = $order; + return $this; + } + + public function getOrder(): int + { + return $this->order; + } + + public function setCreatedAt(?string $created_at): self + { + $this->created_at = $created_at; + return $this; + } + + public function getCreatedAt(): ?string + { + return $this->created_at; + } + + public function setUpdatedAt(?string $updated_at): self + { + $this->updated_at = $updated_at; + return $this; + } + + public function getUpdatedAt(): ?string + { + return $this->updated_at; + } + + public function setDeletedAt(?string $deleted_at): self + { + $this->deleted_at = $deleted_at; + return $this; + } + + public function getDeletedAt(): ?string + { + return $this->deleted_at; + } +} diff --git a/backend/app/DomainObjects/Generated/TicketsCheckInListDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/ProductCheckInListDomainObjectAbstract.php similarity index 73% rename from backend/app/DomainObjects/Generated/TicketsCheckInListDomainObjectAbstract.php rename to backend/app/DomainObjects/Generated/ProductCheckInListDomainObjectAbstract.php index da5209ea..427251d7 100644 --- a/backend/app/DomainObjects/Generated/TicketsCheckInListDomainObjectAbstract.php +++ b/backend/app/DomainObjects/Generated/ProductCheckInListDomainObjectAbstract.php @@ -6,17 +6,17 @@ * THIS FILE IS AUTOGENERATED - DO NOT EDIT IT DIRECTLY. * @package HiEvents\DomainObjects\Generated */ -abstract class TicketsCheckInListDomainObjectAbstract extends \HiEvents\DomainObjects\AbstractDomainObject +abstract class ProductCheckInListDomainObjectAbstract extends \HiEvents\DomainObjects\AbstractDomainObject { - final public const SINGULAR_NAME = 'tickets_check_in_list'; - final public const PLURAL_NAME = 'tickets_check_in_lists'; + final public const SINGULAR_NAME = 'product_check_in_list'; + final public const PLURAL_NAME = 'product_check_in_lists'; final public const ID = 'id'; - final public const TICKET_ID = 'ticket_id'; + final public const PRODUCT_ID = 'product_id'; final public const CHECK_IN_LIST_ID = 'check_in_list_id'; final public const DELETED_AT = 'deleted_at'; protected int $id; - protected int $ticket_id; + protected int $product_id; protected int $check_in_list_id; protected ?string $deleted_at = null; @@ -24,7 +24,7 @@ public function toArray(): array { return [ 'id' => $this->id ?? null, - 'ticket_id' => $this->ticket_id ?? null, + 'product_id' => $this->product_id ?? null, 'check_in_list_id' => $this->check_in_list_id ?? null, 'deleted_at' => $this->deleted_at ?? null, ]; @@ -41,15 +41,15 @@ public function getId(): int return $this->id; } - public function setTicketId(int $ticket_id): self + public function setProductId(int $product_id): self { - $this->ticket_id = $ticket_id; + $this->product_id = $product_id; return $this; } - public function getTicketId(): int + public function getProductId(): int { - return $this->ticket_id; + return $this->product_id; } public function setCheckInListId(int $check_in_list_id): self diff --git a/backend/app/DomainObjects/Generated/TicketDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/ProductDomainObjectAbstract.php similarity index 89% rename from backend/app/DomainObjects/Generated/TicketDomainObjectAbstract.php rename to backend/app/DomainObjects/Generated/ProductDomainObjectAbstract.php index 691ce41e..ca6e0c4f 100644 --- a/backend/app/DomainObjects/Generated/TicketDomainObjectAbstract.php +++ b/backend/app/DomainObjects/Generated/ProductDomainObjectAbstract.php @@ -6,12 +6,13 @@ * THIS FILE IS AUTOGENERATED - DO NOT EDIT IT DIRECTLY. * @package HiEvents\DomainObjects\Generated */ -abstract class TicketDomainObjectAbstract extends \HiEvents\DomainObjects\AbstractDomainObject +abstract class ProductDomainObjectAbstract extends \HiEvents\DomainObjects\AbstractDomainObject { - final public const SINGULAR_NAME = 'ticket'; - final public const PLURAL_NAME = 'tickets'; + final public const SINGULAR_NAME = 'product'; + final public const PLURAL_NAME = 'products'; final public const ID = 'id'; final public const EVENT_ID = 'event_id'; + final public const PRODUCT_CATEGORY_ID = 'product_category_id'; final public const TITLE = 'title'; final public const SALE_START_DATE = 'sale_start_date'; final public const SALE_END_DATE = 'sale_end_date'; @@ -32,9 +33,11 @@ abstract class TicketDomainObjectAbstract extends \HiEvents\DomainObjects\Abstra final public const TYPE = 'type'; final public const IS_HIDDEN = 'is_hidden'; final public const START_COLLAPSED = 'start_collapsed'; + final public const PRODUCT_TYPE = 'product_type'; protected int $id; protected int $event_id; + protected ?int $product_category_id = null; protected string $title; protected ?string $sale_start_date = null; protected ?string $sale_end_date = null; @@ -55,12 +58,14 @@ abstract class TicketDomainObjectAbstract extends \HiEvents\DomainObjects\Abstra protected string $type = 'PAID'; protected ?bool $is_hidden = false; protected bool $start_collapsed = false; + protected string $product_type = 'TICKET'; public function toArray(): array { return [ 'id' => $this->id ?? null, 'event_id' => $this->event_id ?? null, + 'product_category_id' => $this->product_category_id ?? null, 'title' => $this->title ?? null, 'sale_start_date' => $this->sale_start_date ?? null, 'sale_end_date' => $this->sale_end_date ?? null, @@ -81,6 +86,7 @@ public function toArray(): array 'type' => $this->type ?? null, 'is_hidden' => $this->is_hidden ?? null, 'start_collapsed' => $this->start_collapsed ?? null, + 'product_type' => $this->product_type ?? null, ]; } @@ -106,6 +112,17 @@ public function getEventId(): int return $this->event_id; } + public function setProductCategoryId(?int $product_category_id): self + { + $this->product_category_id = $product_category_id; + return $this; + } + + public function getProductCategoryId(): ?int + { + return $this->product_category_id; + } + public function setTitle(string $title): self { $this->title = $title; @@ -325,4 +342,15 @@ public function getStartCollapsed(): bool { return $this->start_collapsed; } + + public function setProductType(string $product_type): self + { + $this->product_type = $product_type; + return $this; + } + + public function getProductType(): string + { + return $this->product_type; + } } diff --git a/backend/app/DomainObjects/Generated/TicketPriceDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/ProductPriceDomainObjectAbstract.php similarity index 90% rename from backend/app/DomainObjects/Generated/TicketPriceDomainObjectAbstract.php rename to backend/app/DomainObjects/Generated/ProductPriceDomainObjectAbstract.php index c6e47272..ee1f1457 100644 --- a/backend/app/DomainObjects/Generated/TicketPriceDomainObjectAbstract.php +++ b/backend/app/DomainObjects/Generated/ProductPriceDomainObjectAbstract.php @@ -6,12 +6,12 @@ * THIS FILE IS AUTOGENERATED - DO NOT EDIT IT DIRECTLY. * @package HiEvents\DomainObjects\Generated */ -abstract class TicketPriceDomainObjectAbstract extends \HiEvents\DomainObjects\AbstractDomainObject +abstract class ProductPriceDomainObjectAbstract extends \HiEvents\DomainObjects\AbstractDomainObject { - final public const SINGULAR_NAME = 'ticket_price'; - final public const PLURAL_NAME = 'ticket_prices'; + final public const SINGULAR_NAME = 'product_price'; + final public const PLURAL_NAME = 'product_prices'; final public const ID = 'id'; - final public const TICKET_ID = 'ticket_id'; + final public const PRODUCT_ID = 'product_id'; final public const PRICE = 'price'; final public const LABEL = 'label'; final public const SALE_START_DATE = 'sale_start_date'; @@ -26,7 +26,7 @@ abstract class TicketPriceDomainObjectAbstract extends \HiEvents\DomainObjects\A final public const QUANTITY_AVAILABLE = 'quantity_available'; protected int $id; - protected int $ticket_id; + protected int $product_id; protected float $price; protected ?string $label = null; protected ?string $sale_start_date = null; @@ -44,7 +44,7 @@ public function toArray(): array { return [ 'id' => $this->id ?? null, - 'ticket_id' => $this->ticket_id ?? null, + 'product_id' => $this->product_id ?? null, 'price' => $this->price ?? null, 'label' => $this->label ?? null, 'sale_start_date' => $this->sale_start_date ?? null, @@ -71,15 +71,15 @@ public function getId(): int return $this->id; } - public function setTicketId(int $ticket_id): self + public function setProductId(int $product_id): self { - $this->ticket_id = $ticket_id; + $this->product_id = $product_id; return $this; } - public function getTicketId(): int + public function getProductId(): int { - return $this->ticket_id; + return $this->product_id; } public function setPrice(float $price): self diff --git a/backend/app/DomainObjects/Generated/TicketQuestionDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/ProductQuestionDomainObjectAbstract.php similarity index 71% rename from backend/app/DomainObjects/Generated/TicketQuestionDomainObjectAbstract.php rename to backend/app/DomainObjects/Generated/ProductQuestionDomainObjectAbstract.php index 7aaba00d..7b3cbebf 100644 --- a/backend/app/DomainObjects/Generated/TicketQuestionDomainObjectAbstract.php +++ b/backend/app/DomainObjects/Generated/ProductQuestionDomainObjectAbstract.php @@ -6,17 +6,17 @@ * THIS FILE IS AUTOGENERATED - DO NOT EDIT IT DIRECTLY. * @package HiEvents\DomainObjects\Generated */ -abstract class TicketQuestionDomainObjectAbstract extends \HiEvents\DomainObjects\AbstractDomainObject +abstract class ProductQuestionDomainObjectAbstract extends \HiEvents\DomainObjects\AbstractDomainObject { - final public const SINGULAR_NAME = 'ticket_question'; - final public const PLURAL_NAME = 'ticket_questions'; + final public const SINGULAR_NAME = 'product_question'; + final public const PLURAL_NAME = 'product_questions'; final public const ID = 'id'; - final public const TICKET_ID = 'ticket_id'; + final public const PRODUCT_ID = 'product_id'; final public const QUESTION_ID = 'question_id'; final public const DELETED_AT = 'deleted_at'; protected int $id; - protected int $ticket_id; + protected int $product_id; protected int $question_id; protected ?string $deleted_at = null; @@ -24,7 +24,7 @@ public function toArray(): array { return [ 'id' => $this->id ?? null, - 'ticket_id' => $this->ticket_id ?? null, + 'product_id' => $this->product_id ?? null, 'question_id' => $this->question_id ?? null, 'deleted_at' => $this->deleted_at ?? null, ]; @@ -41,15 +41,15 @@ public function getId(): int return $this->id; } - public function setTicketId(int $ticket_id): self + public function setProductId(int $product_id): self { - $this->ticket_id = $ticket_id; + $this->product_id = $product_id; return $this; } - public function getTicketId(): int + public function getProductId(): int { - return $this->ticket_id; + return $this->product_id; } public function setQuestionId(int $question_id): self diff --git a/backend/app/DomainObjects/Generated/TicketTaxAndFeesDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/ProductTaxAndFeesDomainObjectAbstract.php similarity index 64% rename from backend/app/DomainObjects/Generated/TicketTaxAndFeesDomainObjectAbstract.php rename to backend/app/DomainObjects/Generated/ProductTaxAndFeesDomainObjectAbstract.php index f4f59d97..6c1b7840 100644 --- a/backend/app/DomainObjects/Generated/TicketTaxAndFeesDomainObjectAbstract.php +++ b/backend/app/DomainObjects/Generated/ProductTaxAndFeesDomainObjectAbstract.php @@ -6,23 +6,23 @@ * THIS FILE IS AUTOGENERATED - DO NOT EDIT IT DIRECTLY. * @package HiEvents\DomainObjects\Generated */ -abstract class TicketTaxAndFeesDomainObjectAbstract extends \HiEvents\DomainObjects\AbstractDomainObject +abstract class ProductTaxAndFeesDomainObjectAbstract extends \HiEvents\DomainObjects\AbstractDomainObject { - final public const SINGULAR_NAME = 'ticket_tax_and_fees'; - final public const PLURAL_NAME = 'ticket_tax_and_fees'; + final public const SINGULAR_NAME = 'product_tax_and_fees'; + final public const PLURAL_NAME = 'product_tax_and_fees'; final public const ID = 'id'; - final public const TICKET_ID = 'ticket_id'; + final public const PRODUCT_ID = 'product_id'; final public const TAX_AND_FEE_ID = 'tax_and_fee_id'; protected int $id; - protected int $ticket_id; + protected int $product_id; protected int $tax_and_fee_id; public function toArray(): array { return [ 'id' => $this->id ?? null, - 'ticket_id' => $this->ticket_id ?? null, + 'product_id' => $this->product_id ?? null, 'tax_and_fee_id' => $this->tax_and_fee_id ?? null, ]; } @@ -38,15 +38,15 @@ public function getId(): int return $this->id; } - public function setTicketId(int $ticket_id): self + public function setProductId(int $product_id): self { - $this->ticket_id = $ticket_id; + $this->product_id = $product_id; return $this; } - public function getTicketId(): int + public function getProductId(): int { - return $this->ticket_id; + return $this->product_id; } public function setTaxAndFeeId(int $tax_and_fee_id): self diff --git a/backend/app/DomainObjects/Generated/PromoCodeDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/PromoCodeDomainObjectAbstract.php index f4bda2b5..c1e7174e 100644 --- a/backend/app/DomainObjects/Generated/PromoCodeDomainObjectAbstract.php +++ b/backend/app/DomainObjects/Generated/PromoCodeDomainObjectAbstract.php @@ -14,7 +14,7 @@ abstract class PromoCodeDomainObjectAbstract extends \HiEvents\DomainObjects\Abs final public const EVENT_ID = 'event_id'; final public const CODE = 'code'; final public const DISCOUNT = 'discount'; - final public const APPLICABLE_TICKET_IDS = 'applicable_ticket_ids'; + final public const APPLICABLE_PRODUCT_IDS = 'applicable_product_ids'; final public const EXPIRY_DATE = 'expiry_date'; final public const DISCOUNT_TYPE = 'discount_type'; final public const ATTENDEE_USAGE_COUNT = 'attendee_usage_count'; @@ -28,7 +28,7 @@ abstract class PromoCodeDomainObjectAbstract extends \HiEvents\DomainObjects\Abs protected int $event_id; protected string $code; protected float $discount = 0.0; - protected array|string|null $applicable_ticket_ids = null; + protected array|string|null $applicable_product_ids = null; protected ?string $expiry_date = null; protected ?string $discount_type = null; protected int $attendee_usage_count = 0; @@ -45,7 +45,7 @@ public function toArray(): array 'event_id' => $this->event_id ?? null, 'code' => $this->code ?? null, 'discount' => $this->discount ?? null, - 'applicable_ticket_ids' => $this->applicable_ticket_ids ?? null, + 'applicable_product_ids' => $this->applicable_product_ids ?? null, 'expiry_date' => $this->expiry_date ?? null, 'discount_type' => $this->discount_type ?? null, 'attendee_usage_count' => $this->attendee_usage_count ?? null, @@ -101,15 +101,15 @@ public function getDiscount(): float return $this->discount; } - public function setApplicableTicketIds(array|string|null $applicable_ticket_ids): self + public function setApplicableProductIds(array|string|null $applicable_product_ids): self { - $this->applicable_ticket_ids = $applicable_ticket_ids; + $this->applicable_product_ids = $applicable_product_ids; return $this; } - public function getApplicableTicketIds(): array|string|null + public function getApplicableProductIds(): array|string|null { - return $this->applicable_ticket_ids; + return $this->applicable_product_ids; } public function setExpiryDate(?string $expiry_date): self diff --git a/backend/app/DomainObjects/Generated/QuestionAnswerDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/QuestionAnswerDomainObjectAbstract.php index f71fc5ae..0505f94c 100644 --- a/backend/app/DomainObjects/Generated/QuestionAnswerDomainObjectAbstract.php +++ b/backend/app/DomainObjects/Generated/QuestionAnswerDomainObjectAbstract.php @@ -14,7 +14,7 @@ abstract class QuestionAnswerDomainObjectAbstract extends \HiEvents\DomainObject final public const QUESTION_ID = 'question_id'; final public const ORDER_ID = 'order_id'; final public const ATTENDEE_ID = 'attendee_id'; - final public const TICKET_ID = 'ticket_id'; + final public const PRODUCT_ID = 'product_id'; final public const CREATED_AT = 'created_at'; final public const UPDATED_AT = 'updated_at'; final public const DELETED_AT = 'deleted_at'; @@ -24,7 +24,7 @@ abstract class QuestionAnswerDomainObjectAbstract extends \HiEvents\DomainObject protected int $question_id; protected int $order_id; protected ?int $attendee_id = null; - protected ?int $ticket_id = null; + protected ?int $product_id = null; protected string $created_at; protected string $updated_at; protected ?string $deleted_at = null; @@ -37,7 +37,7 @@ public function toArray(): array 'question_id' => $this->question_id ?? null, 'order_id' => $this->order_id ?? null, 'attendee_id' => $this->attendee_id ?? null, - 'ticket_id' => $this->ticket_id ?? null, + 'product_id' => $this->product_id ?? null, 'created_at' => $this->created_at ?? null, 'updated_at' => $this->updated_at ?? null, 'deleted_at' => $this->deleted_at ?? null, @@ -89,15 +89,15 @@ public function getAttendeeId(): ?int return $this->attendee_id; } - public function setTicketId(?int $ticket_id): self + public function setProductId(?int $product_id): self { - $this->ticket_id = $ticket_id; + $this->product_id = $product_id; return $this; } - public function getTicketId(): ?int + public function getProductId(): ?int { - return $this->ticket_id; + return $this->product_id; } public function setCreatedAt(string $created_at): self diff --git a/backend/app/DomainObjects/Generated/TicketCheckInListDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/TicketCheckInListDomainObjectAbstract.php deleted file mode 100644 index 6b4722ad..00000000 --- a/backend/app/DomainObjects/Generated/TicketCheckInListDomainObjectAbstract.php +++ /dev/null @@ -1,76 +0,0 @@ - $this->id ?? null, - 'ticket_id' => $this->ticket_id ?? null, - 'check_in_list_id' => $this->check_in_list_id ?? null, - 'deleted_at' => $this->deleted_at ?? null, - ]; - } - - public function setId(int $id): self - { - $this->id = $id; - return $this; - } - - public function getId(): int - { - return $this->id; - } - - public function setTicketId(int $ticket_id): self - { - $this->ticket_id = $ticket_id; - return $this; - } - - public function getTicketId(): int - { - return $this->ticket_id; - } - - public function setCheckInListId(int $check_in_list_id): self - { - $this->check_in_list_id = $check_in_list_id; - return $this; - } - - public function getCheckInListId(): int - { - return $this->check_in_list_id; - } - - public function setDeletedAt(?string $deleted_at): self - { - $this->deleted_at = $deleted_at; - return $this; - } - - public function getDeletedAt(): ?string - { - return $this->deleted_at; - } -} diff --git a/backend/app/DomainObjects/Generated/TicketTaxDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/TicketTaxDomainObjectAbstract.php deleted file mode 100644 index 66295f55..00000000 --- a/backend/app/DomainObjects/Generated/TicketTaxDomainObjectAbstract.php +++ /dev/null @@ -1,62 +0,0 @@ - $this->id ?? null, - 'ticket_id' => $this->ticket_id ?? null, - 'tax_id' => $this->tax_id ?? null, - ]; - } - - public function setId(int $id): self - { - $this->id = $id; - return $this; - } - - public function getId(): int - { - return $this->id; - } - - public function setTicketId(int $ticket_id): self - { - $this->ticket_id = $ticket_id; - return $this; - } - - public function getTicketId(): int - { - return $this->ticket_id; - } - - public function setTaxId(int $tax_id): self - { - $this->tax_id = $tax_id; - return $this; - } - - public function getTaxId(): int - { - return $this->tax_id; - } -} diff --git a/backend/app/DomainObjects/OrderDomainObject.php b/backend/app/DomainObjects/OrderDomainObject.php index eb6bdd60..b196bb5e 100644 --- a/backend/app/DomainObjects/OrderDomainObject.php +++ b/backend/app/DomainObjects/OrderDomainObject.php @@ -2,6 +2,7 @@ namespace HiEvents\DomainObjects; +use HiEvents\DomainObjects\Enums\ProductType; use HiEvents\DomainObjects\Interfaces\IsSortable; use HiEvents\DomainObjects\SortingAndFiltering\AllowedSorts; use HiEvents\DomainObjects\Status\OrderPaymentStatus; @@ -66,6 +67,28 @@ public function getFullName(): string return $this->getFirstName() . ' ' . $this->getLastName(); } + public function getProductOrderItems(): Collection + { + if ($this->getOrderItems() === null) { + return new Collection(); + } + + return $this->getOrderItems()->filter(static function (OrderItemDomainObject $orderItem) { + return $orderItem->getProductType() === ProductType::GENERAL->name; + }); + } + + public function getTicketOrderItems(): Collection + { + if ($this->getOrderItems() === null) { + return new Collection(); + } + + return $this->getOrderItems()->filter(static function (OrderItemDomainObject $orderItem) { + return $orderItem->getProductType() === ProductType::TICKET->name; + }); + } + public function setOrderItems(?Collection $orderItems): OrderDomainObject { $this->orderItems = $orderItems; @@ -124,7 +147,7 @@ public function isPartiallyRefunded(): bool public function isFullyRefunded(): bool { - return $this->getTotalRefunded() >= $this->getTotalGross(); + return !$this->isFreeOrder() && ($this->getTotalRefunded() >= $this->getTotalGross()); } public function getStripePayment(): ?StripePaymentDomainObject diff --git a/backend/app/DomainObjects/OrderItemDomainObject.php b/backend/app/DomainObjects/OrderItemDomainObject.php index 1a93d71e..164b1d9c 100644 --- a/backend/app/DomainObjects/OrderItemDomainObject.php +++ b/backend/app/DomainObjects/OrderItemDomainObject.php @@ -6,35 +6,49 @@ class OrderItemDomainObject extends Generated\OrderItemDomainObjectAbstract { - private ?TicketPriceDomainObject $ticketPrice = null; + private ?ProductPriceDomainObject $productPrice = null; - public ?TicketDomainObject $ticket = null; + public ?ProductDomainObject $product = null; + + public ?OrderDomainObject $order = null; public function getTotalBeforeDiscount(): float { return Currency::round($this->getPriceBeforeDiscount() * $this->getQuantity()); } - public function getTicketPrice(): ?TicketPriceDomainObject + public function getProductPrice(): ?ProductPriceDomainObject + { + return $this->productPrice; + } + + public function setProductPrice(?ProductPriceDomainObject $tier): self + { + $this->productPrice = $tier; + + return $this; + } + + public function getProduct(): ?ProductDomainObject { - return $this->ticketPrice; + return $this->product; } - public function setTicketPrice(?TicketPriceDomainObject $tier): self + public function setProduct(?ProductDomainObject $product): self { - $this->ticketPrice = $tier; + $this->product = $product; return $this; } - public function getTicket(): ?TicketDomainObject + public function getOrder(): ?OrderDomainObject { - return $this->ticket; + return $this->order; } - public function setTicket(?TicketDomainObject $ticket): self + public function setOrder(?OrderDomainObject $order): self { - $this->ticket = $ticket; + $this->order = $order; return $this; } diff --git a/backend/app/DomainObjects/ProductCapacityAssignmentDomainObject.php b/backend/app/DomainObjects/ProductCapacityAssignmentDomainObject.php new file mode 100644 index 00000000..6560231b --- /dev/null +++ b/backend/app/DomainObjects/ProductCapacityAssignmentDomainObject.php @@ -0,0 +1,7 @@ +products = $products; + } + + public function getProducts(): ?Collection + { + return $this->products; + } +} diff --git a/backend/app/DomainObjects/TicketsCheckInListDomainObject.php b/backend/app/DomainObjects/ProductCheckInListDomainObject.php similarity index 54% rename from backend/app/DomainObjects/TicketsCheckInListDomainObject.php rename to backend/app/DomainObjects/ProductCheckInListDomainObject.php index 9e10d8b0..77ef4598 100644 --- a/backend/app/DomainObjects/TicketsCheckInListDomainObject.php +++ b/backend/app/DomainObjects/ProductCheckInListDomainObject.php @@ -2,6 +2,6 @@ namespace HiEvents\DomainObjects; -class TicketsCheckInListDomainObject extends Generated\TicketsCheckInListDomainObjectAbstract +class ProductCheckInListDomainObject extends Generated\ProductCheckInListDomainObjectAbstract { } diff --git a/backend/app/DomainObjects/TicketDomainObject.php b/backend/app/DomainObjects/ProductDomainObject.php similarity index 66% rename from backend/app/DomainObjects/TicketDomainObject.php rename to backend/app/DomainObjects/ProductDomainObject.php index 718a5cc6..bf54e4f8 100644 --- a/backend/app/DomainObjects/TicketDomainObject.php +++ b/backend/app/DomainObjects/ProductDomainObject.php @@ -3,14 +3,14 @@ namespace HiEvents\DomainObjects; use Carbon\Carbon; +use HiEvents\DomainObjects\Enums\ProductPriceType; use HiEvents\Constants; -use HiEvents\DomainObjects\Enums\TicketType; use HiEvents\DomainObjects\Interfaces\IsSortable; use HiEvents\DomainObjects\SortingAndFiltering\AllowedSorts; use Illuminate\Support\Collection; use LogicException; -class TicketDomainObject extends Generated\TicketDomainObjectAbstract implements IsSortable +class ProductDomainObject extends Generated\ProductDomainObjectAbstract implements IsSortable { private ?Collection $taxAndFees = null; @@ -57,7 +57,7 @@ public static function getAllowedSorts(): AllowedSorts ); } - public function setTaxAndFees(Collection $taxes): TicketDomainObject + public function setTaxAndFees(Collection $taxes): ProductDomainObject { $this->taxAndFees = $taxes; return $this; @@ -80,36 +80,36 @@ public function getFees(): ?Collection public function isSoldOut(): bool { - if (!$this->getTicketPrices() || $this->getTicketPrices()->isEmpty()) { + if (!$this->getProductPrices() || $this->getProductPrices()->isEmpty()) { return true; } - return $this->getTicketPrices()->every(fn(TicketPriceDomainObject $price) => $price->isSoldOut()); + return $this->getProductPrices()->every(fn(ProductPriceDomainObject $price) => $price->isSoldOut()); } public function getQuantityAvailable(): int { - $availableCount = $this->getTicketPrices()->sum(fn(TicketPriceDomainObject $price) => $price->getQuantityAvailable()); + $availableCount = $this->getProductPrices()->sum(fn(ProductPriceDomainObject $price) => $price->getQuantityAvailable()); if ($this->quantityAvailable !== null) { return min($availableCount, $this->quantityAvailable); } - if (!$this->getTicketPrices() || $this->getTicketPrices()->isEmpty()) { + if (!$this->getProductPrices() || $this->getProductPrices()->isEmpty()) { return 0; } // This is to address a case where prices have an unlimited quantity available and the user has // enabled show_quantity_remaining. if ($this->getShowQuantityRemaining() - && $this->getTicketPrices()->first(fn(TicketPriceDomainObject $price) => $price->getQuantityAvailable() === null)) { + && $this->getProductPrices()->first(fn(ProductPriceDomainObject $price) => $price->getQuantityAvailable() === null)) { return Constants::INFINITE; } return $availableCount; } - public function setQuantityAvailable(int $quantityAvailable): TicketDomainObject + public function setQuantityAvailable(int $quantityAvailable): ProductDomainObject { $this->quantityAvailable = $quantityAvailable; @@ -133,7 +133,7 @@ public function isAfterSaleEndDate(): bool public function isAvailable(): bool { // If all prices are hidden, it's not available - if ($this->getType() === TicketType::TIERED->name && $this->getTicketPrices()?->isEmpty()) { + if ($this->getType() === ProductPriceType::TIERED->name && $this->getProductPrices()?->isEmpty()) { return false; } @@ -144,14 +144,14 @@ public function isAvailable(): bool } /** - * @return Collection|null + * @return Collection|null */ - public function getTicketPrices(): ?Collection + public function getProductPrices(): ?Collection { return $this->prices; } - public function setTicketPrices(?Collection $prices): self + public function setProductPrices(?Collection $prices): self { $this->prices = $prices; @@ -159,59 +159,59 @@ public function setTicketPrices(?Collection $prices): self } /** - * All ticket types except TIERED have a single price, so we can just return the first price. + * All product types except TIERED have a single price, so we can just return the first price. * * @return float|null */ public function getPrice(): ?float { - if ($this->getType() === TicketType::TIERED->name) { - throw new LogicException('You cannot get a single price for a tiered ticket. Use getPrices() instead.'); + if ($this->getType() === ProductPriceType::TIERED->name) { + throw new LogicException('You cannot get a single price for a tiered product. Use getPrices() instead.'); } - return $this->getTicketPrices()?->first()->getPrice(); + return $this->getProductPrices()?->first()->getPrice(); } - public function getPriceById(int $priceId): ?TicketPriceDomainObject + public function getPriceById(int $priceId): ?ProductPriceDomainObject { - return $this->getTicketPrices()?->first(fn(TicketPriceDomainObject $price) => $price->getId() === $priceId); + return $this->getProductPrices()?->first(fn(ProductPriceDomainObject $price) => $price->getId() === $priceId); } public function isTieredType(): bool { - return $this->getType() === TicketType::TIERED->name; + return $this->getType() === ProductPriceType::TIERED->name; } public function isDonationType(): bool { - return $this->getType() === TicketType::DONATION->name; + return $this->getType() === ProductPriceType::DONATION->name; } public function isPaidType(): bool { - return $this->getType() === TicketType::PAID->name; + return $this->getType() === ProductPriceType::PAID->name; } public function isFreeType(): bool { - return $this->getType() === TicketType::FREE->name; + return $this->getType() === ProductPriceType::FREE->name; } public function getInitialQuantityAvailable(): ?int { - if ($this->getType() === TicketType::TIERED->name) { - return $this->getTicketPrices()?->sum(fn(TicketPriceDomainObject $price) => $price->getInitialQuantityAvailable()); + if ($this->getType() === ProductPriceType::TIERED->name) { + return $this->getProductPrices()?->sum(fn(ProductPriceDomainObject $price) => $price->getInitialQuantityAvailable()); } - return $this->getTicketPrices()?->first()?->getInitialQuantityAvailable(); + return $this->getProductPrices()?->first()?->getInitialQuantityAvailable(); } public function getQuantitySold(): int { - return $this->getTicketPrices()?->sum(fn(TicketPriceDomainObject $price) => $price->getQuantitySold()) ?? 0; + return $this->getProductPrices()?->sum(fn(ProductPriceDomainObject $price) => $price->getQuantitySold()) ?? 0; } - public function setOffSaleReason(?string $offSaleReason): TicketDomainObject + public function setOffSaleReason(?string $offSaleReason): ProductDomainObject { $this->offSaleReason = $offSaleReason; diff --git a/backend/app/DomainObjects/TicketPriceDomainObject.php b/backend/app/DomainObjects/ProductPriceDomainObject.php similarity index 89% rename from backend/app/DomainObjects/TicketPriceDomainObject.php rename to backend/app/DomainObjects/ProductPriceDomainObject.php index d2d312e9..0919781e 100644 --- a/backend/app/DomainObjects/TicketPriceDomainObject.php +++ b/backend/app/DomainObjects/ProductPriceDomainObject.php @@ -6,7 +6,7 @@ use HiEvents\Helper\Currency; use LogicException; -class TicketPriceDomainObject extends Generated\TicketPriceDomainObjectAbstract +class ProductPriceDomainObject extends Generated\ProductPriceDomainObjectAbstract { private ?float $priceBeforeDiscount = null; @@ -23,7 +23,7 @@ public function getPriceBeforeDiscount(): ?float return $this->priceBeforeDiscount; } - public function setPriceBeforeDiscount(?float $originalPrice): TicketPriceDomainObject + public function setPriceBeforeDiscount(?float $originalPrice): ProductPriceDomainObject { $this->priceBeforeDiscount = $originalPrice; @@ -96,13 +96,13 @@ public function isAvailable(): ?bool return $this->isAvailable; } - public function setIsAvailable(?bool $isAvailable): TicketPriceDomainObject + public function setIsAvailable(?bool $isAvailable): ProductPriceDomainObject { $this->isAvailable = $isAvailable; return $this; } - public function setOffSaleReason(?string $offSaleReason): TicketPriceDomainObject + public function setOffSaleReason(?string $offSaleReason): ProductPriceDomainObject { $this->offSaleReason = $offSaleReason; diff --git a/backend/app/DomainObjects/ProductQuestionDomainObject.php b/backend/app/DomainObjects/ProductQuestionDomainObject.php new file mode 100644 index 00000000..866ff446 --- /dev/null +++ b/backend/app/DomainObjects/ProductQuestionDomainObject.php @@ -0,0 +1,7 @@ +getApplicableTicketIds()) { + // If there's no product IDs we apply the promo to all products + if (!$this->getApplicableProductIds()) { return true; } - return in_array($ticket->getId(), array_map('intval', $this->getApplicableTicketIds()), true); + return in_array($product->getId(), array_map('intval', $this->getApplicableProductIds()), true); } public function isFixedDiscount(): bool diff --git a/backend/app/DomainObjects/QuestionAndAnswerViewDomainObject.php b/backend/app/DomainObjects/QuestionAndAnswerViewDomainObject.php index b0af8664..41abc43f 100644 --- a/backend/app/DomainObjects/QuestionAndAnswerViewDomainObject.php +++ b/backend/app/DomainObjects/QuestionAndAnswerViewDomainObject.php @@ -10,6 +10,8 @@ class QuestionAndAnswerViewDomainObject extends AbstractDomainObject final public const SINGULAR_NAME = 'question_and_answer_view'; final public const PLURAL_NAME = 'question_and_answer_views'; + private ?int $product_id; + private ?string $product_title; private int $question_id; private ?int $order_id; private string $title; @@ -131,6 +133,28 @@ public function setEventId(int $event_id): QuestionAndAnswerViewDomainObject return $this; } + public function getProductId(): ?int + { + return $this->product_id; + } + + public function setProductId(?int $product_id): QuestionAndAnswerViewDomainObject + { + $this->product_id = $product_id; + return $this; + } + + public function getProductTitle(): ?string + { + return $this->product_title; + } + + public function setProductTitle(?string $product_title): QuestionAndAnswerViewDomainObject + { + $this->product_title = $product_title; + return $this; + } + public function toArray(): array { return [ diff --git a/backend/app/DomainObjects/QuestionDomainObject.php b/backend/app/DomainObjects/QuestionDomainObject.php index 6ddd5a13..5b544cc9 100644 --- a/backend/app/DomainObjects/QuestionDomainObject.php +++ b/backend/app/DomainObjects/QuestionDomainObject.php @@ -7,24 +7,26 @@ class QuestionDomainObject extends Generated\QuestionDomainObjectAbstract { - public ?Collection $tickets = null; + public ?Collection $products = null; - public function setTickets(?Collection $tickets): QuestionDomainObject + public function setProducts(?Collection $products): QuestionDomainObject { - $this->tickets = $tickets; + $this->products = $products; return $this; } - public function getTickets(): ?Collection + public function getProducts(): ?Collection { - return $this->tickets; + return $this->products; } - public function isMultipleChoice(): bool + public function isPreDefinedChoice(): bool { return in_array($this->getType(), [ - QuestionTypeEnum::MULTI_SELECT_DROPDOWN, - QuestionTypeEnum::CHECKBOX, + QuestionTypeEnum::MULTI_SELECT_DROPDOWN->name, + QuestionTypeEnum::CHECKBOX->name, + QuestionTypeEnum::RADIO->name, + QuestionTypeEnum::DROPDOWN->name, ], true); } diff --git a/backend/app/DomainObjects/Status/TicketStatus.php b/backend/app/DomainObjects/Status/ProductStatus.php similarity index 88% rename from backend/app/DomainObjects/Status/TicketStatus.php rename to backend/app/DomainObjects/Status/ProductStatus.php index 2f6e2bfa..fe55a347 100644 --- a/backend/app/DomainObjects/Status/TicketStatus.php +++ b/backend/app/DomainObjects/Status/ProductStatus.php @@ -4,7 +4,7 @@ use HiEvents\DomainObjects\Enums\BaseEnum; -enum TicketStatus +enum ProductStatus { use BaseEnum; diff --git a/backend/app/DomainObjects/TicketAttributeDomainObject.php b/backend/app/DomainObjects/TicketAttributeDomainObject.php deleted file mode 100644 index 00aeeee0..00000000 --- a/backend/app/DomainObjects/TicketAttributeDomainObject.php +++ /dev/null @@ -1,7 +0,0 @@ -json([ 'message' => $exception->getMessage() ?: 'Resource not found', ], 404); + } else if ($exception instanceof MissingAbilityException) { + return response()->json([ + 'message' => $exception->getMessage(), + ], 403); } return parent::render($request, $exception); diff --git a/backend/app/Exceptions/InvalidTicketPriceId.php b/backend/app/Exceptions/InvalidProductPriceId.php similarity index 56% rename from backend/app/Exceptions/InvalidTicketPriceId.php rename to backend/app/Exceptions/InvalidProductPriceId.php index 2edf9d98..162fe268 100644 --- a/backend/app/Exceptions/InvalidTicketPriceId.php +++ b/backend/app/Exceptions/InvalidProductPriceId.php @@ -4,7 +4,7 @@ use Exception; -class InvalidTicketPriceId extends Exception +class InvalidProductPriceId extends Exception { } diff --git a/backend/app/Exports/AttendeesExport.php b/backend/app/Exports/AttendeesExport.php index 7f5d0d42..73448844 100644 --- a/backend/app/Exports/AttendeesExport.php +++ b/backend/app/Exports/AttendeesExport.php @@ -4,11 +4,11 @@ use Carbon\Carbon; use HiEvents\DomainObjects\AttendeeDomainObject; +use HiEvents\DomainObjects\Enums\ProductPriceType; use HiEvents\DomainObjects\Enums\QuestionTypeEnum; -use HiEvents\DomainObjects\Enums\TicketType; +use HiEvents\DomainObjects\ProductDomainObject; +use HiEvents\DomainObjects\ProductPriceDomainObject; use HiEvents\DomainObjects\QuestionDomainObject; -use HiEvents\DomainObjects\TicketDomainObject; -use HiEvents\DomainObjects\TicketPriceDomainObject; use HiEvents\Resources\Attendee\AttendeeResource; use HiEvents\Services\Domain\Question\QuestionAnswerFormatter; use Illuminate\Contracts\Pagination\LengthAwarePaginator; @@ -53,8 +53,8 @@ public function headings(): array 'Status', 'Is Checked In', 'Checked In At', - 'Ticket ID', - 'Ticket Name', + 'Product ID', + 'Product Name', 'Event ID', 'Public ID', 'Short ID', @@ -79,13 +79,13 @@ public function map($attendee): array ); }); - /** @var TicketDomainObject $ticket */ - $ticket = $attendee->getTicket(); + /** @var ProductDomainObject $ticket */ + $ticket = $attendee->getProduct(); $ticketName = $ticket->getTitle(); - if ($attendee->getTicket()?->getType() === TicketType::TIERED->name) { + if ($ticket->getType() === ProductPriceType::TIERED->name) { $ticketName .= ' - ' . $ticket - ->getTicketPrices() - ->first(fn(TicketPriceDomainObject $tp) => $tp->getId() === $attendee->getTicketPriceId()) + ->getProductPrices() + ->first(fn(ProductPriceDomainObject $tp) => $tp->getId() === $attendee->getProductPriceId()) ->getLabel(); } @@ -99,7 +99,7 @@ public function map($attendee): array $attendee->getCheckIn() ? Carbon::parse($attendee->getCheckIn()->getCreatedAt())->format('Y-m-d H:i:s') : '', - $attendee->getTicketId(), + $attendee->getProductId(), $ticketName, $attendee->getEventId(), $attendee->getPublicId(), diff --git a/backend/app/Helper/Url.php b/backend/app/Helper/Url.php index 4eb96a6e..aff22c54 100644 --- a/backend/app/Helper/Url.php +++ b/backend/app/Helper/Url.php @@ -11,7 +11,7 @@ class Url public const ACCEPT_INVITATION = 'app.frontend_urls.accept_invitation'; public const CONFIRM_EMAIL_ADDRESS = 'app.frontend_urls.confirm_email_address'; public const EVENT_HOMEPAGE = 'app.frontend_urls.event_homepage'; - public const ATTENDEE_TICKET = 'app.frontend_urls.attendee_ticket'; + public const ATTENDEE_TICKET = 'app.frontend_urls.attendee_product'; public const ORDER_SUMMARY = 'app.frontend_urls.order_summary'; public const ORGANIZER_ORDER_SUMMARY = 'app.frontend_urls.organizer_order_summary'; diff --git a/backend/app/Http/Actions/Accounts/CreateAccountAction.php b/backend/app/Http/Actions/Accounts/CreateAccountAction.php index 26f94ea0..36365f95 100644 --- a/backend/app/Http/Actions/Accounts/CreateAccountAction.php +++ b/backend/app/Http/Actions/Accounts/CreateAccountAction.php @@ -10,12 +10,12 @@ use HiEvents\Http\Request\Account\CreateAccountRequest; use HiEvents\Http\ResponseCodes; use HiEvents\Resources\Account\AccountResource; +use HiEvents\Services\Application\Handlers\Account\CreateAccountHandler; +use HiEvents\Services\Application\Handlers\Account\DTO\CreateAccountDTO; +use HiEvents\Services\Application\Handlers\Account\Exceptions\AccountRegistrationDisabledException; +use HiEvents\Services\Application\Handlers\Auth\DTO\LoginCredentialsDTO; +use HiEvents\Services\Application\Handlers\Auth\LoginHandler; use HiEvents\Services\Application\Locale\LocaleService; -use HiEvents\Services\Handlers\Account\CreateAccountHandler; -use HiEvents\Services\Handlers\Account\DTO\CreateAccountDTO; -use HiEvents\Services\Handlers\Account\Exceptions\AccountRegistrationDisabledException; -use HiEvents\Services\Handlers\Auth\DTO\LoginCredentialsDTO; -use HiEvents\Services\Handlers\Auth\LoginHandler; use Illuminate\Http\JsonResponse; use Illuminate\Validation\ValidationException; use Throwable; diff --git a/backend/app/Http/Actions/Accounts/Stripe/CreateStripeConnectAccountAction.php b/backend/app/Http/Actions/Accounts/Stripe/CreateStripeConnectAccountAction.php index 5018f8e5..ca3f429b 100644 --- a/backend/app/Http/Actions/Accounts/Stripe/CreateStripeConnectAccountAction.php +++ b/backend/app/Http/Actions/Accounts/Stripe/CreateStripeConnectAccountAction.php @@ -9,8 +9,8 @@ use HiEvents\Exceptions\SaasModeEnabledException; use HiEvents\Http\Actions\BaseAction; use HiEvents\Resources\Account\Stripe\StripeConnectAccountResponseResource; -use HiEvents\Services\Handlers\Account\Payment\Stripe\CreateStripeConnectAccountHandler; -use HiEvents\Services\Handlers\Account\Payment\Stripe\DTO\CreateStripeConnectAccountDTO; +use HiEvents\Services\Application\Handlers\Account\Payment\Stripe\CreateStripeConnectAccountHandler; +use HiEvents\Services\Application\Handlers\Account\Payment\Stripe\DTO\CreateStripeConnectAccountDTO; use Illuminate\Http\JsonResponse; use Symfony\Component\HttpFoundation\Response; use Throwable; diff --git a/backend/app/Http/Actions/Accounts/UpdateAccountAction.php b/backend/app/Http/Actions/Accounts/UpdateAccountAction.php index 082281f9..a4fd2040 100644 --- a/backend/app/Http/Actions/Accounts/UpdateAccountAction.php +++ b/backend/app/Http/Actions/Accounts/UpdateAccountAction.php @@ -6,8 +6,8 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\Account\UpdateAccountRequest; use HiEvents\Resources\Account\AccountResource; -use HiEvents\Services\Handlers\Account\DTO\UpdateAccountDTO; -use HiEvents\Services\Handlers\Account\UpdateAccountHanlder; +use HiEvents\Services\Application\Handlers\Account\DTO\UpdateAccountDTO; +use HiEvents\Services\Application\Handlers\Account\UpdateAccountHanlder; use Illuminate\Http\JsonResponse; class UpdateAccountAction extends BaseAction diff --git a/backend/app/Http/Actions/Attendees/CheckInAttendeeAction.php b/backend/app/Http/Actions/Attendees/CheckInAttendeeAction.php index f5f6ece6..2717865e 100644 --- a/backend/app/Http/Actions/Attendees/CheckInAttendeeAction.php +++ b/backend/app/Http/Actions/Attendees/CheckInAttendeeAction.php @@ -8,8 +8,8 @@ use HiEvents\Http\Request\Attendee\CheckInAttendeeRequest; use HiEvents\Http\ResponseCodes; use HiEvents\Resources\Attendee\AttendeeResource; -use HiEvents\Services\Handlers\Attendee\CheckInAttendeeHandler; -use HiEvents\Services\Handlers\Attendee\DTO\CheckInAttendeeDTO; +use HiEvents\Services\Application\Handlers\Attendee\CheckInAttendeeHandler; +use HiEvents\Services\Application\Handlers\Attendee\DTO\CheckInAttendeeDTO; use Illuminate\Http\JsonResponse; class CheckInAttendeeAction extends BaseAction diff --git a/backend/app/Http/Actions/Attendees/CreateAttendeeAction.php b/backend/app/Http/Actions/Attendees/CreateAttendeeAction.php index e43a9776..492b0651 100644 --- a/backend/app/Http/Actions/Attendees/CreateAttendeeAction.php +++ b/backend/app/Http/Actions/Attendees/CreateAttendeeAction.php @@ -3,14 +3,14 @@ namespace HiEvents\Http\Actions\Attendees; use HiEvents\DomainObjects\EventDomainObject; -use HiEvents\Exceptions\InvalidTicketPriceId; +use HiEvents\Exceptions\InvalidProductPriceId; use HiEvents\Exceptions\NoTicketsAvailableException; use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\Attendee\CreateAttendeeRequest; use HiEvents\Http\ResponseCodes; use HiEvents\Resources\Attendee\AttendeeResource; -use HiEvents\Services\Handlers\Attendee\CreateAttendeeHandler; -use HiEvents\Services\Handlers\Attendee\DTO\CreateAttendeeDTO; +use HiEvents\Services\Application\Handlers\Attendee\CreateAttendeeHandler; +use HiEvents\Services\Application\Handlers\Attendee\DTO\CreateAttendeeDTO; use Illuminate\Http\JsonResponse; use Illuminate\Validation\ValidationException; use Throwable; @@ -39,11 +39,11 @@ public function __invoke(CreateAttendeeRequest $request, int $eventId): JsonResp )); } catch (NoTicketsAvailableException $exception) { throw ValidationException::withMessages([ - 'ticket_id' => $exception->getMessage(), + 'product_id' => $exception->getMessage(), ]); - } catch (InvalidTicketPriceId $exception) { + } catch (InvalidProductPriceId $exception) { throw ValidationException::withMessages([ - 'ticket_price_id' => $exception->getMessage(), + 'product_price_id' => $exception->getMessage(), ]); } diff --git a/backend/app/Http/Actions/Attendees/EditAttendeeAction.php b/backend/app/Http/Actions/Attendees/EditAttendeeAction.php index c9f11f6d..ede204d4 100644 --- a/backend/app/Http/Actions/Attendees/EditAttendeeAction.php +++ b/backend/app/Http/Actions/Attendees/EditAttendeeAction.php @@ -7,8 +7,8 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\Attendee\EditAttendeeRequest; use HiEvents\Resources\Attendee\AttendeeResource; -use HiEvents\Services\Handlers\Attendee\DTO\EditAttendeeDTO; -use HiEvents\Services\Handlers\Attendee\EditAttendeeHandler; +use HiEvents\Services\Application\Handlers\Attendee\DTO\EditAttendeeDTO; +use HiEvents\Services\Application\Handlers\Attendee\EditAttendeeHandler; use Illuminate\Http\JsonResponse; use Illuminate\Validation\ValidationException; use Throwable; @@ -33,14 +33,15 @@ public function __invoke(EditAttendeeRequest $request, int $eventId, int $attend 'first_name' => $request->input('first_name'), 'last_name' => $request->input('last_name'), 'email' => $request->input('email'), - 'ticket_id' => $request->input('ticket_id'), - 'ticket_price_id' => $request->input('ticket_price_id'), + 'product_id' => $request->input('product_id'), + 'product_price_id' => $request->input('product_price_id'), 'event_id' => $eventId, 'attendee_id' => $attendeeId, + 'notes' => $request->input('notes'), ])); } catch (NoTicketsAvailableException $exception) { throw ValidationException::withMessages([ - 'ticket_id' => $exception->getMessage(), + 'product_id' => $exception->getMessage(), ]); } diff --git a/backend/app/Http/Actions/Attendees/ExportAttendeesAction.php b/backend/app/Http/Actions/Attendees/ExportAttendeesAction.php index 729b5c8a..c6f28da3 100644 --- a/backend/app/Http/Actions/Attendees/ExportAttendeesAction.php +++ b/backend/app/Http/Actions/Attendees/ExportAttendeesAction.php @@ -52,7 +52,7 @@ public function __invoke(int $eventId): BinaryFileResponse $questions = $this->questionRepository->findWhere([ 'event_id' => $eventId, - 'belongs_to' => QuestionBelongsTo::TICKET->name, + 'belongs_to' => QuestionBelongsTo::PRODUCT->name, ]); return Excel::download( diff --git a/backend/app/Http/Actions/Attendees/GetAttendeeAction.php b/backend/app/Http/Actions/Attendees/GetAttendeeAction.php index f022a734..fdf2a210 100644 --- a/backend/app/Http/Actions/Attendees/GetAttendeeAction.php +++ b/backend/app/Http/Actions/Attendees/GetAttendeeAction.php @@ -4,9 +4,9 @@ use HiEvents\DomainObjects\AttendeeCheckInDomainObject; use HiEvents\DomainObjects\EventDomainObject; +use HiEvents\DomainObjects\ProductDomainObject; +use HiEvents\DomainObjects\ProductPriceDomainObject; use HiEvents\DomainObjects\QuestionAndAnswerViewDomainObject; -use HiEvents\DomainObjects\TicketDomainObject; -use HiEvents\DomainObjects\TicketPriceDomainObject; use HiEvents\Http\Actions\BaseAction; use HiEvents\Repository\Eloquent\Value\Relationship; use HiEvents\Repository\Interfaces\AttendeeRepositoryInterface; @@ -30,12 +30,12 @@ public function __invoke(int $eventId, int $attendeeId): Response|JsonResponse $attendee = $this->attendeeRepository ->loadRelation(relationship: QuestionAndAnswerViewDomainObject::class) ->loadRelation(new Relationship( - domainObject: TicketDomainObject::class, + domainObject: ProductDomainObject::class, nested: [ new Relationship( - domainObject: TicketPriceDomainObject::class, + domainObject: ProductPriceDomainObject::class, ), - ], name: 'ticket')) + ], name: 'product')) ->loadRelation(new Relationship( domainObject: AttendeeCheckInDomainObject::class, name: 'check_in', diff --git a/backend/app/Http/Actions/Attendees/GetAttendeeActionPublic.php b/backend/app/Http/Actions/Attendees/GetAttendeeActionPublic.php index 87d0b2f8..5be7ddf3 100644 --- a/backend/app/Http/Actions/Attendees/GetAttendeeActionPublic.php +++ b/backend/app/Http/Actions/Attendees/GetAttendeeActionPublic.php @@ -3,8 +3,8 @@ namespace HiEvents\Http\Actions\Attendees; use HiEvents\DomainObjects\Generated\AttendeeDomainObjectAbstract; -use HiEvents\DomainObjects\TicketDomainObject; -use HiEvents\DomainObjects\TicketPriceDomainObject; +use HiEvents\DomainObjects\ProductDomainObject; +use HiEvents\DomainObjects\ProductPriceDomainObject; use HiEvents\Http\Actions\BaseAction; use HiEvents\Repository\Eloquent\Value\Relationship; use HiEvents\Repository\Interfaces\AttendeeRepositoryInterface; @@ -28,12 +28,12 @@ public function __invoke(int $eventId, string $attendeeShortId): JsonResponse|Re { $attendee = $this->attendeeRepository ->loadRelation(new Relationship( - domainObject: TicketDomainObject::class, + domainObject: ProductDomainObject::class, nested: [ new Relationship( - domainObject: TicketPriceDomainObject::class, + domainObject: ProductPriceDomainObject::class, ), - ], name: 'ticket')) + ], name: 'product')) ->findFirstWhere([ AttendeeDomainObjectAbstract::SHORT_ID => $attendeeShortId ]); diff --git a/backend/app/Http/Actions/Attendees/PartialEditAttendeeAction.php b/backend/app/Http/Actions/Attendees/PartialEditAttendeeAction.php index 16d308a4..e62d0dbd 100644 --- a/backend/app/Http/Actions/Attendees/PartialEditAttendeeAction.php +++ b/backend/app/Http/Actions/Attendees/PartialEditAttendeeAction.php @@ -6,8 +6,8 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\Attendee\PartialEditAttendeeRequest; use HiEvents\Resources\Attendee\AttendeeResource; -use HiEvents\Services\Handlers\Attendee\DTO\PartialEditAttendeeDTO; -use HiEvents\Services\Handlers\Attendee\PartialEditAttendeeHandler; +use HiEvents\Services\Application\Handlers\Attendee\DTO\PartialEditAttendeeDTO; +use HiEvents\Services\Application\Handlers\Attendee\PartialEditAttendeeHandler; use Illuminate\Http\JsonResponse; class PartialEditAttendeeAction extends BaseAction diff --git a/backend/app/Http/Actions/Attendees/ResendAttendeeTicketAction.php b/backend/app/Http/Actions/Attendees/ResendAttendeeTicketAction.php index 9a6c1295..191fdb88 100644 --- a/backend/app/Http/Actions/Attendees/ResendAttendeeTicketAction.php +++ b/backend/app/Http/Actions/Attendees/ResendAttendeeTicketAction.php @@ -4,8 +4,8 @@ use HiEvents\DomainObjects\EventDomainObject; use HiEvents\Http\Actions\BaseAction; -use HiEvents\Services\Handlers\Attendee\DTO\ResendAttendeeTicketDTO; -use HiEvents\Services\Handlers\Attendee\ResendAttendeeTicketHandler; +use HiEvents\Services\Application\Handlers\Attendee\DTO\ResendAttendeeTicketDTO; +use HiEvents\Services\Application\Handlers\Attendee\ResendAttendeeTicketHandler; use Illuminate\Http\JsonResponse; use Illuminate\Http\Response; use Symfony\Component\Routing\Exception\ResourceNotFoundException; diff --git a/backend/app/Http/Actions/Auth/AcceptInvitationAction.php b/backend/app/Http/Actions/Auth/AcceptInvitationAction.php index 90d2f606..ca147ee9 100644 --- a/backend/app/Http/Actions/Auth/AcceptInvitationAction.php +++ b/backend/app/Http/Actions/Auth/AcceptInvitationAction.php @@ -6,8 +6,8 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\Auth\AcceptInvitationRequest; use HiEvents\Http\ResponseCodes; -use HiEvents\Services\Handlers\Auth\AcceptInvitationHandler; -use HiEvents\Services\Handlers\Auth\DTO\AcceptInvitationDTO; +use HiEvents\Services\Application\Handlers\Auth\AcceptInvitationHandler; +use HiEvents\Services\Application\Handlers\Auth\DTO\AcceptInvitationDTO; use HiEvents\Services\Infrastructure\Encryption\Exception\DecryptionFailedException; use HiEvents\Services\Infrastructure\Encryption\Exception\EncryptedPayloadExpiredException; use Illuminate\Http\Response; diff --git a/backend/app/Http/Actions/Auth/BaseAuthAction.php b/backend/app/Http/Actions/Auth/BaseAuthAction.php index d11699a1..e5c5ecac 100644 --- a/backend/app/Http/Actions/Auth/BaseAuthAction.php +++ b/backend/app/Http/Actions/Auth/BaseAuthAction.php @@ -4,7 +4,7 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Resources\Auth\AuthenticatedResponseResource; -use HiEvents\Services\Handlers\Auth\DTO\AuthenticatedResponseDTO; +use HiEvents\Services\Application\Handlers\Auth\DTO\AuthenticatedResponseDTO; use Illuminate\Http\JsonResponse; use Illuminate\Http\Response; use Illuminate\Support\Collection; diff --git a/backend/app/Http/Actions/Auth/CreateApiKeyAction.php b/backend/app/Http/Actions/Auth/CreateApiKeyAction.php new file mode 100644 index 00000000..85d3f2b9 --- /dev/null +++ b/backend/app/Http/Actions/Auth/CreateApiKeyAction.php @@ -0,0 +1,31 @@ +minimumAllowedRole(Role::ADMIN); + + $abilities = ['*']; + $expiryDateTime = null; + if ($request->abilities && count($request->abilities) > 0) { + $abilities = $request->abilities; + } + if ($request->expires_at) { + $expiryDateTime = DateTime::createFromFormat("U", strtotime($request->expires_at)); + } + return $this->jsonResponse($request->user()->createToken( + $request->token_name, + $abilities, + $expiryDateTime)); + } +} \ No newline at end of file diff --git a/backend/app/Http/Actions/Auth/ForgotPasswordAction.php b/backend/app/Http/Actions/Auth/ForgotPasswordAction.php index 693cbdab..e272b2ce 100644 --- a/backend/app/Http/Actions/Auth/ForgotPasswordAction.php +++ b/backend/app/Http/Actions/Auth/ForgotPasswordAction.php @@ -4,7 +4,7 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\Auth\ForgotPasswordRequest; -use HiEvents\Services\Handlers\Auth\ForgotPasswordHandler; +use HiEvents\Services\Application\Handlers\Auth\ForgotPasswordHandler; use Illuminate\Http\JsonResponse; use Symfony\Component\Routing\Exception\ResourceNotFoundException; diff --git a/backend/app/Http/Actions/Auth/GetApiKeysAction.php b/backend/app/Http/Actions/Auth/GetApiKeysAction.php new file mode 100644 index 00000000..e5ad4209 --- /dev/null +++ b/backend/app/Http/Actions/Auth/GetApiKeysAction.php @@ -0,0 +1,20 @@ +minimumAllowedRole(Role::ADMIN); + + $tokens = $request->user()->tokens; + return $this->jsonResponse($tokens); + } +} \ No newline at end of file diff --git a/backend/app/Http/Actions/Auth/LoginAction.php b/backend/app/Http/Actions/Auth/LoginAction.php index eee4960a..e5f3b178 100644 --- a/backend/app/Http/Actions/Auth/LoginAction.php +++ b/backend/app/Http/Actions/Auth/LoginAction.php @@ -7,8 +7,8 @@ use HiEvents\Exceptions\UnauthorizedException; use HiEvents\Http\Request\Auth\LoginRequest; use HiEvents\Http\ResponseCodes; -use HiEvents\Services\Handlers\Auth\DTO\LoginCredentialsDTO; -use HiEvents\Services\Handlers\Auth\LoginHandler; +use HiEvents\Services\Application\Handlers\Auth\DTO\LoginCredentialsDTO; +use HiEvents\Services\Application\Handlers\Auth\LoginHandler; use Illuminate\Http\JsonResponse; class LoginAction extends BaseAuthAction diff --git a/backend/app/Http/Actions/Auth/ResetPasswordAction.php b/backend/app/Http/Actions/Auth/ResetPasswordAction.php index d7425d33..6e7b25d8 100644 --- a/backend/app/Http/Actions/Auth/ResetPasswordAction.php +++ b/backend/app/Http/Actions/Auth/ResetPasswordAction.php @@ -5,8 +5,8 @@ use HiEvents\Exceptions\PasswordInvalidException; use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\Auth\ResetPasswordRequest; -use HiEvents\Services\Handlers\Auth\DTO\ResetPasswordDTO; -use HiEvents\Services\Handlers\Auth\ResetPasswordHandler; +use HiEvents\Services\Application\Handlers\Auth\DTO\ResetPasswordDTO; +use HiEvents\Services\Application\Handlers\Auth\ResetPasswordHandler; use Illuminate\Http\JsonResponse; use Illuminate\Validation\ValidationException; use Symfony\Component\Routing\Exception\ResourceNotFoundException; diff --git a/backend/app/Http/Actions/Auth/RevokeApiKeyAction.php b/backend/app/Http/Actions/Auth/RevokeApiKeyAction.php new file mode 100644 index 00000000..dc52bb18 --- /dev/null +++ b/backend/app/Http/Actions/Auth/RevokeApiKeyAction.php @@ -0,0 +1,24 @@ +minimumAllowedRole(Role::ADMIN); + + if ($request->user()->tokens()->where('id', $apiKey)->delete()) { + return $this->deletedResponse(); + } else { + return $this->notFoundResponse(); + } + } +} \ No newline at end of file diff --git a/backend/app/Http/Actions/Auth/ValidateResetPasswordTokenAction.php b/backend/app/Http/Actions/Auth/ValidateResetPasswordTokenAction.php index 51e6a1c4..8516bb79 100644 --- a/backend/app/Http/Actions/Auth/ValidateResetPasswordTokenAction.php +++ b/backend/app/Http/Actions/Auth/ValidateResetPasswordTokenAction.php @@ -4,7 +4,7 @@ use HiEvents\Exceptions\InvalidPasswordResetTokenException; use HiEvents\Http\Actions\BaseAction; -use HiEvents\Services\Handlers\Auth\ValidateResetPasswordTokenHandler; +use HiEvents\Services\Application\Handlers\Auth\ValidateResetPasswordTokenHandler; use Illuminate\Http\Request; use Illuminate\Http\Response; use Symfony\Component\Routing\Exception\ResourceNotFoundException; diff --git a/backend/app/Http/Actions/CapacityAssignments/CreateCapacityAssignmentAction.php b/backend/app/Http/Actions/CapacityAssignments/CreateCapacityAssignmentAction.php index 0eae8c0d..d9cc6e1d 100644 --- a/backend/app/Http/Actions/CapacityAssignments/CreateCapacityAssignmentAction.php +++ b/backend/app/Http/Actions/CapacityAssignments/CreateCapacityAssignmentAction.php @@ -6,9 +6,9 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\CapacityAssigment\UpsertCapacityAssignmentRequest; use HiEvents\Resources\CapacityAssignment\CapacityAssignmentResource; -use HiEvents\Services\Domain\Ticket\Exception\UnrecognizedTicketIdException; -use HiEvents\Services\Handlers\CapacityAssignment\CreateCapacityAssignmentHandler; -use HiEvents\Services\Handlers\CapacityAssignment\DTO\UpsertCapacityAssignmentDTO; +use HiEvents\Services\Application\Handlers\CapacityAssignment\CreateCapacityAssignmentHandler; +use HiEvents\Services\Application\Handlers\CapacityAssignment\DTO\UpsertCapacityAssignmentDTO; +use HiEvents\Services\Domain\Product\Exception\UnrecognizedProductIdException; use Illuminate\Http\JsonResponse; use Symfony\Component\HttpFoundation\Response; @@ -31,10 +31,10 @@ public function __invoke(int $eventId, UpsertCapacityAssignmentRequest $request) 'event_id' => $eventId, 'capacity' => $request->validated('capacity'), 'status' => $request->validated('status'), - 'ticket_ids' => $request->validated('ticket_ids'), + 'product_ids' => $request->validated('product_ids'), ]), ); - } catch (UnrecognizedTicketIdException $exception) { + } catch (UnrecognizedProductIdException $exception) { return $this->errorResponse( message: $exception->getMessage(), statusCode: Response::HTTP_UNPROCESSABLE_ENTITY, diff --git a/backend/app/Http/Actions/CapacityAssignments/DeleteCapacityAssignmentAction.php b/backend/app/Http/Actions/CapacityAssignments/DeleteCapacityAssignmentAction.php index 0a11c156..e31d6927 100644 --- a/backend/app/Http/Actions/CapacityAssignments/DeleteCapacityAssignmentAction.php +++ b/backend/app/Http/Actions/CapacityAssignments/DeleteCapacityAssignmentAction.php @@ -4,7 +4,7 @@ use HiEvents\DomainObjects\EventDomainObject; use HiEvents\Http\Actions\BaseAction; -use HiEvents\Services\Handlers\CapacityAssignment\DeleteCapacityAssignmentHandler; +use HiEvents\Services\Application\Handlers\CapacityAssignment\DeleteCapacityAssignmentHandler; use Illuminate\Http\Response; class DeleteCapacityAssignmentAction extends BaseAction diff --git a/backend/app/Http/Actions/CapacityAssignments/GetCapacityAssignmentAction.php b/backend/app/Http/Actions/CapacityAssignments/GetCapacityAssignmentAction.php index 15b61aaa..676cb218 100644 --- a/backend/app/Http/Actions/CapacityAssignments/GetCapacityAssignmentAction.php +++ b/backend/app/Http/Actions/CapacityAssignments/GetCapacityAssignmentAction.php @@ -5,7 +5,7 @@ use HiEvents\DomainObjects\EventDomainObject; use HiEvents\Http\Actions\BaseAction; use HiEvents\Resources\CapacityAssignment\CapacityAssignmentResource; -use HiEvents\Services\Handlers\CapacityAssignment\GetCapacityAssignmentHandler; +use HiEvents\Services\Application\Handlers\CapacityAssignment\GetCapacityAssignmentHandler; use Illuminate\Http\JsonResponse; class GetCapacityAssignmentAction extends BaseAction diff --git a/backend/app/Http/Actions/CapacityAssignments/GetCapacityAssignmentsAction.php b/backend/app/Http/Actions/CapacityAssignments/GetCapacityAssignmentsAction.php index 35ae5893..f6c4f791 100644 --- a/backend/app/Http/Actions/CapacityAssignments/GetCapacityAssignmentsAction.php +++ b/backend/app/Http/Actions/CapacityAssignments/GetCapacityAssignmentsAction.php @@ -6,8 +6,8 @@ use HiEvents\DomainObjects\EventDomainObject; use HiEvents\Http\Actions\BaseAction; use HiEvents\Resources\CapacityAssignment\CapacityAssignmentResource; -use HiEvents\Services\Handlers\CapacityAssignment\DTO\GetCapacityAssignmentsDTO; -use HiEvents\Services\Handlers\CapacityAssignment\GetCapacityAssignmentsHandler; +use HiEvents\Services\Application\Handlers\CapacityAssignment\DTO\GetCapacityAssignmentsDTO; +use HiEvents\Services\Application\Handlers\CapacityAssignment\GetCapacityAssignmentsHandler; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; diff --git a/backend/app/Http/Actions/CapacityAssignments/UpdateCapacityAssignmentAction.php b/backend/app/Http/Actions/CapacityAssignments/UpdateCapacityAssignmentAction.php index 0deb55fb..b80035d7 100644 --- a/backend/app/Http/Actions/CapacityAssignments/UpdateCapacityAssignmentAction.php +++ b/backend/app/Http/Actions/CapacityAssignments/UpdateCapacityAssignmentAction.php @@ -6,9 +6,9 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\CapacityAssigment\UpsertCapacityAssignmentRequest; use HiEvents\Resources\CapacityAssignment\CapacityAssignmentResource; -use HiEvents\Services\Domain\Ticket\Exception\UnrecognizedTicketIdException; -use HiEvents\Services\Handlers\CapacityAssignment\DTO\UpsertCapacityAssignmentDTO; -use HiEvents\Services\Handlers\CapacityAssignment\UpdateCapacityAssignmentHandler; +use HiEvents\Services\Application\Handlers\CapacityAssignment\DTO\UpsertCapacityAssignmentDTO; +use HiEvents\Services\Application\Handlers\CapacityAssignment\UpdateCapacityAssignmentHandler; +use HiEvents\Services\Domain\Product\Exception\UnrecognizedProductIdException; use Illuminate\Http\JsonResponse; use Symfony\Component\HttpFoundation\Response; @@ -33,10 +33,10 @@ public function __invoke(int $eventId, int $capacityAssignmentId, UpsertCapacity 'capacity' => $request->validated('capacity'), 'applies_to' => $request->validated('applies_to'), 'status' => $request->validated('status'), - 'ticket_ids' => $request->validated('ticket_ids'), + 'product_ids' => $request->validated('product_ids'), ]), ); - } catch (UnrecognizedTicketIdException $exception) { + } catch (UnrecognizedProductIdException $exception) { return $this->errorResponse( message: $exception->getMessage(), statusCode: Response::HTTP_UNPROCESSABLE_ENTITY, diff --git a/backend/app/Http/Actions/CheckInLists/CreateCheckInListAction.php b/backend/app/Http/Actions/CheckInLists/CreateCheckInListAction.php index c10ada72..19a96260 100644 --- a/backend/app/Http/Actions/CheckInLists/CreateCheckInListAction.php +++ b/backend/app/Http/Actions/CheckInLists/CreateCheckInListAction.php @@ -5,9 +5,9 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\CheckInList\UpsertCheckInListRequest; use HiEvents\Resources\CheckInList\CheckInListResource; -use HiEvents\Services\Domain\Ticket\Exception\UnrecognizedTicketIdException; -use HiEvents\Services\Handlers\CheckInList\CreateCheckInListHandler; -use HiEvents\Services\Handlers\CheckInList\DTO\UpsertCheckInListDTO; +use HiEvents\Services\Application\Handlers\CheckInList\CreateCheckInListHandler; +use HiEvents\Services\Application\Handlers\CheckInList\DTO\UpsertCheckInListDTO; +use HiEvents\Services\Domain\Product\Exception\UnrecognizedProductIdException; use Illuminate\Http\JsonResponse; use Illuminate\Http\Response; @@ -27,12 +27,12 @@ public function __invoke(UpsertCheckInListRequest $request, int $eventId): JsonR name: $request->validated('name'), description: $request->validated('description'), eventId: $eventId, - ticketIds: $request->validated('ticket_ids'), + productIds: $request->validated('product_ids'), expiresAt: $request->validated('expires_at'), activatesAt: $request->validated('activates_at'), ) ); - } catch (UnrecognizedTicketIdException $exception) { + } catch (UnrecognizedProductIdException $exception) { return $this->errorResponse( message: $exception->getMessage(), statusCode: Response::HTTP_UNPROCESSABLE_ENTITY, diff --git a/backend/app/Http/Actions/CheckInLists/DeleteCheckInListAction.php b/backend/app/Http/Actions/CheckInLists/DeleteCheckInListAction.php index 6341109e..f707a073 100644 --- a/backend/app/Http/Actions/CheckInLists/DeleteCheckInListAction.php +++ b/backend/app/Http/Actions/CheckInLists/DeleteCheckInListAction.php @@ -4,7 +4,7 @@ use HiEvents\DomainObjects\EventDomainObject; use HiEvents\Http\Actions\BaseAction; -use HiEvents\Services\Handlers\CheckInList\DeleteCheckInListHandler; +use HiEvents\Services\Application\Handlers\CheckInList\DeleteCheckInListHandler; use Illuminate\Http\Response; class DeleteCheckInListAction extends BaseAction diff --git a/backend/app/Http/Actions/CheckInLists/GetCheckInListAction.php b/backend/app/Http/Actions/CheckInLists/GetCheckInListAction.php index eb7eb0e5..6e60ce9d 100644 --- a/backend/app/Http/Actions/CheckInLists/GetCheckInListAction.php +++ b/backend/app/Http/Actions/CheckInLists/GetCheckInListAction.php @@ -5,7 +5,7 @@ use HiEvents\DomainObjects\EventDomainObject; use HiEvents\Http\Actions\BaseAction; use HiEvents\Resources\CheckInList\CheckInListResource; -use HiEvents\Services\Handlers\CheckInList\GetCheckInListHandler; +use HiEvents\Services\Application\Handlers\CheckInList\GetCheckInListHandler; use Illuminate\Http\JsonResponse; class GetCheckInListAction extends BaseAction diff --git a/backend/app/Http/Actions/CheckInLists/GetCheckInListsAction.php b/backend/app/Http/Actions/CheckInLists/GetCheckInListsAction.php index 137f22e5..bc7ca01d 100644 --- a/backend/app/Http/Actions/CheckInLists/GetCheckInListsAction.php +++ b/backend/app/Http/Actions/CheckInLists/GetCheckInListsAction.php @@ -6,8 +6,8 @@ use HiEvents\DomainObjects\EventDomainObject; use HiEvents\Http\Actions\BaseAction; use HiEvents\Resources\CheckInList\CheckInListResource; -use HiEvents\Services\Handlers\CheckInList\DTO\GetCheckInListsDTO; -use HiEvents\Services\Handlers\CheckInList\GetCheckInListsHandler; +use HiEvents\Services\Application\Handlers\CheckInList\DTO\GetCheckInListsDTO; +use HiEvents\Services\Application\Handlers\CheckInList\GetCheckInListsHandler; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; diff --git a/backend/app/Http/Actions/CheckInLists/Public/CreateAttendeeCheckInPublicAction.php b/backend/app/Http/Actions/CheckInLists/Public/CreateAttendeeCheckInPublicAction.php index d3d3ae77..981ad4f9 100644 --- a/backend/app/Http/Actions/CheckInLists/Public/CreateAttendeeCheckInPublicAction.php +++ b/backend/app/Http/Actions/CheckInLists/Public/CreateAttendeeCheckInPublicAction.php @@ -6,8 +6,8 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\CheckInList\CreateAttendeeCheckInPublicRequest; use HiEvents\Resources\CheckInList\AttendeeCheckInPublicResource; -use HiEvents\Services\Handlers\CheckInList\Public\CreateAttendeeCheckInPublicHandler; -use HiEvents\Services\Handlers\CheckInList\Public\DTO\CreateAttendeeCheckInPublicDTO; +use HiEvents\Services\Application\Handlers\CheckInList\Public\CreateAttendeeCheckInPublicHandler; +use HiEvents\Services\Application\Handlers\CheckInList\Public\DTO\CreateAttendeeCheckInPublicDTO; use Illuminate\Http\JsonResponse; use Illuminate\Http\Response; diff --git a/backend/app/Http/Actions/CheckInLists/Public/DeleteAttendeeCheckInPublicAction.php b/backend/app/Http/Actions/CheckInLists/Public/DeleteAttendeeCheckInPublicAction.php index 6062e043..f19abe52 100644 --- a/backend/app/Http/Actions/CheckInLists/Public/DeleteAttendeeCheckInPublicAction.php +++ b/backend/app/Http/Actions/CheckInLists/Public/DeleteAttendeeCheckInPublicAction.php @@ -4,8 +4,8 @@ use HiEvents\Exceptions\CannotCheckInException; use HiEvents\Http\Actions\BaseAction; -use HiEvents\Services\Handlers\CheckInList\Public\DeleteAttendeeCheckInPublicHandler; -use HiEvents\Services\Handlers\CheckInList\Public\DTO\DeleteAttendeeCheckInPublicDTO; +use HiEvents\Services\Application\Handlers\CheckInList\Public\DeleteAttendeeCheckInPublicHandler; +use HiEvents\Services\Application\Handlers\CheckInList\Public\DTO\DeleteAttendeeCheckInPublicDTO; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; diff --git a/backend/app/Http/Actions/CheckInLists/Public/GetCheckInListAttendeesPublicAction.php b/backend/app/Http/Actions/CheckInLists/Public/GetCheckInListAttendeesPublicAction.php index 5ca18186..bffce8b0 100644 --- a/backend/app/Http/Actions/CheckInLists/Public/GetCheckInListAttendeesPublicAction.php +++ b/backend/app/Http/Actions/CheckInLists/Public/GetCheckInListAttendeesPublicAction.php @@ -6,7 +6,7 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\DTO\QueryParamsDTO; use HiEvents\Resources\Attendee\AttendeeWithCheckInPublicResource; -use HiEvents\Services\Handlers\CheckInList\Public\GetCheckInListAttendeesPublicHandler; +use HiEvents\Services\Application\Handlers\CheckInList\Public\GetCheckInListAttendeesPublicHandler; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; diff --git a/backend/app/Http/Actions/CheckInLists/Public/GetCheckInListPublicAction.php b/backend/app/Http/Actions/CheckInLists/Public/GetCheckInListPublicAction.php index 0b711f9d..fded28bf 100644 --- a/backend/app/Http/Actions/CheckInLists/Public/GetCheckInListPublicAction.php +++ b/backend/app/Http/Actions/CheckInLists/Public/GetCheckInListPublicAction.php @@ -4,7 +4,7 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Resources\CheckInList\CheckInListResourcePublic; -use HiEvents\Services\Handlers\CheckInList\Public\GetCheckInListPublicHandler; +use HiEvents\Services\Application\Handlers\CheckInList\Public\GetCheckInListPublicHandler; use Illuminate\Http\JsonResponse; class GetCheckInListPublicAction extends BaseAction diff --git a/backend/app/Http/Actions/CheckInLists/UpdateCheckInListAction.php b/backend/app/Http/Actions/CheckInLists/UpdateCheckInListAction.php index 5599617d..dceda8c8 100644 --- a/backend/app/Http/Actions/CheckInLists/UpdateCheckInListAction.php +++ b/backend/app/Http/Actions/CheckInLists/UpdateCheckInListAction.php @@ -6,9 +6,9 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\CheckInList\UpsertCheckInListRequest; use HiEvents\Resources\CheckInList\CheckInListResource; -use HiEvents\Services\Domain\Ticket\Exception\UnrecognizedTicketIdException; -use HiEvents\Services\Handlers\CheckInList\DTO\UpsertCheckInListDTO; -use HiEvents\Services\Handlers\CheckInList\UpdateCheckInlistHandler; +use HiEvents\Services\Application\Handlers\CheckInList\DTO\UpsertCheckInListDTO; +use HiEvents\Services\Application\Handlers\CheckInList\UpdateCheckInlistHandler; +use HiEvents\Services\Domain\Product\Exception\UnrecognizedProductIdException; use Illuminate\Http\JsonResponse; use Symfony\Component\HttpFoundation\Response; @@ -30,13 +30,13 @@ public function __invoke(UpsertCheckInListRequest $request, int $eventId, int $c name: $request->validated('name'), description: $request->validated('description'), eventId: $eventId, - ticketIds: $request->validated('ticket_ids'), + productIds: $request->validated('product_ids'), expiresAt: $request->validated('expires_at'), activatesAt: $request->validated('activates_at'), id: $checkInListId, ) ); - } catch (UnrecognizedTicketIdException $exception) { + } catch (UnrecognizedProductIdException $exception) { return $this->errorResponse( message: $exception->getMessage(), statusCode: Response::HTTP_UNPROCESSABLE_ENTITY, diff --git a/backend/app/Http/Actions/Common/Webhooks/StripeIncomingWebhookAction.php b/backend/app/Http/Actions/Common/Webhooks/StripeIncomingWebhookAction.php index 075883d8..869d8021 100644 --- a/backend/app/Http/Actions/Common/Webhooks/StripeIncomingWebhookAction.php +++ b/backend/app/Http/Actions/Common/Webhooks/StripeIncomingWebhookAction.php @@ -4,8 +4,8 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\ResponseCodes; -use HiEvents\Services\Handlers\Order\Payment\Stripe\DTO\StripeWebhookDTO; -use HiEvents\Services\Handlers\Order\Payment\Stripe\IncomingWebhookHandler; +use HiEvents\Services\Application\Handlers\Order\Payment\Stripe\DTO\StripeWebhookDTO; +use HiEvents\Services\Application\Handlers\Order\Payment\Stripe\IncomingWebhookHandler; use Illuminate\Http\Request; use Illuminate\Http\Response; use Throwable; diff --git a/backend/app/Http/Actions/EventSettings/EditEventSettingsAction.php b/backend/app/Http/Actions/EventSettings/EditEventSettingsAction.php index 610c100e..ff97d978 100644 --- a/backend/app/Http/Actions/EventSettings/EditEventSettingsAction.php +++ b/backend/app/Http/Actions/EventSettings/EditEventSettingsAction.php @@ -6,8 +6,8 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\EventSettings\UpdateEventSettingsRequest; use HiEvents\Resources\Event\EventSettingsResource; -use HiEvents\Services\Handlers\EventSettings\DTO\UpdateEventSettingsDTO; -use HiEvents\Services\Handlers\EventSettings\UpdateEventSettingsHandler; +use HiEvents\Services\Application\Handlers\EventSettings\DTO\UpdateEventSettingsDTO; +use HiEvents\Services\Application\Handlers\EventSettings\UpdateEventSettingsHandler; use Illuminate\Http\JsonResponse; class EditEventSettingsAction extends BaseAction diff --git a/backend/app/Http/Actions/EventSettings/PartialEditEventSettingsAction.php b/backend/app/Http/Actions/EventSettings/PartialEditEventSettingsAction.php index 93dcb200..09b20aab 100644 --- a/backend/app/Http/Actions/EventSettings/PartialEditEventSettingsAction.php +++ b/backend/app/Http/Actions/EventSettings/PartialEditEventSettingsAction.php @@ -6,8 +6,8 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\EventSettings\UpdateEventSettingsRequest; use HiEvents\Resources\Event\EventSettingsResource; -use HiEvents\Services\Handlers\EventSettings\DTO\PartialUpdateEventSettingsDTO; -use HiEvents\Services\Handlers\EventSettings\PartialUpdateEventSettingsHandler; +use HiEvents\Services\Application\Handlers\EventSettings\DTO\PartialUpdateEventSettingsDTO; +use HiEvents\Services\Application\Handlers\EventSettings\PartialUpdateEventSettingsHandler; use Illuminate\Http\JsonResponse; use Throwable; diff --git a/backend/app/Http/Actions/Events/CreateEventAction.php b/backend/app/Http/Actions/Events/CreateEventAction.php index 997d253f..088a90d0 100644 --- a/backend/app/Http/Actions/Events/CreateEventAction.php +++ b/backend/app/Http/Actions/Events/CreateEventAction.php @@ -6,8 +6,8 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\Event\CreateEventRequest; use HiEvents\Resources\Event\EventResource; -use HiEvents\Services\Handlers\Event\CreateEventHandler; -use HiEvents\Services\Handlers\Event\DTO\CreateEventDTO; +use HiEvents\Services\Application\Handlers\Event\CreateEventHandler; +use HiEvents\Services\Application\Handlers\Event\DTO\CreateEventDTO; use Illuminate\Http\JsonResponse; use Illuminate\Validation\ValidationException; use Throwable; diff --git a/backend/app/Http/Actions/Events/DuplicateEventAction.php b/backend/app/Http/Actions/Events/DuplicateEventAction.php index 2f6063da..cd0cf4e9 100644 --- a/backend/app/Http/Actions/Events/DuplicateEventAction.php +++ b/backend/app/Http/Actions/Events/DuplicateEventAction.php @@ -6,8 +6,8 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\Event\DuplicateEventRequest; use HiEvents\Resources\Event\EventResource; +use HiEvents\Services\Application\Handlers\Event\DuplicateEventHandler; use HiEvents\Services\Domain\Event\DTO\DuplicateEventDataDTO; -use HiEvents\Services\Handlers\Event\DuplicateEventHandler; use Illuminate\Http\JsonResponse; use Throwable; @@ -29,7 +29,7 @@ public function __invoke(int $eventId, DuplicateEventRequest $request): JsonResp accountId: $this->getAuthenticatedAccountId(), title: $request->validated('title'), startDate: $request->validated('start_date'), - duplicateTickets: $request->validated('duplicate_tickets'), + duplicateProducts: $request->validated('duplicate_products'), duplicateQuestions: $request->validated('duplicate_questions'), duplicateSettings: $request->validated('duplicate_settings'), duplicatePromoCodes: $request->validated('duplicate_promo_codes'), diff --git a/backend/app/Http/Actions/Events/GetEventAction.php b/backend/app/Http/Actions/Events/GetEventAction.php index d2ac6f9c..f1ca7611 100644 --- a/backend/app/Http/Actions/Events/GetEventAction.php +++ b/backend/app/Http/Actions/Events/GetEventAction.php @@ -6,9 +6,10 @@ use HiEvents\DomainObjects\EventDomainObject; use HiEvents\DomainObjects\OrganizerDomainObject; +use HiEvents\DomainObjects\ProductCategoryDomainObject; use HiEvents\DomainObjects\TaxAndFeesDomainObject; -use HiEvents\DomainObjects\TicketDomainObject; -use HiEvents\DomainObjects\TicketPriceDomainObject; +use HiEvents\DomainObjects\ProductDomainObject; +use HiEvents\DomainObjects\ProductPriceDomainObject; use HiEvents\Http\Actions\BaseAction; use HiEvents\Repository\Eloquent\Value\Relationship; use HiEvents\Repository\Interfaces\EventRepositoryInterface; @@ -31,10 +32,12 @@ public function __invoke(int $eventId): JsonResponse $event = $this->eventRepository ->loadRelation(new Relationship(domainObject: OrganizerDomainObject::class, name: 'organizer')) ->loadRelation( - new Relationship(TicketDomainObject::class, [ - new Relationship(TicketPriceDomainObject::class), - new Relationship(TaxAndFeesDomainObject::class), - ]), + new Relationship(ProductCategoryDomainObject::class, [ + new Relationship(ProductDomainObject::class, [ + new Relationship(ProductPriceDomainObject::class), + new Relationship(TaxAndFeesDomainObject::class), + ]), + ]) ) ->findById($eventId); diff --git a/backend/app/Http/Actions/Events/GetEventPublicAction.php b/backend/app/Http/Actions/Events/GetEventPublicAction.php index 14472026..540153fe 100644 --- a/backend/app/Http/Actions/Events/GetEventPublicAction.php +++ b/backend/app/Http/Actions/Events/GetEventPublicAction.php @@ -5,8 +5,8 @@ use HiEvents\DomainObjects\Status\EventStatus; use HiEvents\Http\Actions\BaseAction; use HiEvents\Resources\Event\EventResourcePublic; -use HiEvents\Services\Handlers\Event\DTO\GetPublicEventDTO; -use HiEvents\Services\Handlers\Event\GetPublicEventHandler; +use HiEvents\Services\Application\Handlers\Event\DTO\GetPublicEventDTO; +use HiEvents\Services\Application\Handlers\Event\GetPublicEventHandler; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; @@ -15,7 +15,7 @@ class GetEventPublicAction extends BaseAction { public function __construct( - private readonly GetPublicEventHandler $handler, + private readonly GetPublicEventHandler $getPublicEventHandler, private readonly LoggerInterface $logger, ) { @@ -23,7 +23,7 @@ public function __construct( public function __invoke(int $eventId, Request $request): Response|JsonResponse { - $event = $this->handler->handle(GetPublicEventDTO::fromArray([ + $event = $this->getPublicEventHandler->handle(GetPublicEventDTO::fromArray([ 'eventId' => $eventId, 'ipAddress' => $this->getClientIp($request), 'promoCode' => strtolower($request->string('promo_code')), diff --git a/backend/app/Http/Actions/Events/GetEventsAction.php b/backend/app/Http/Actions/Events/GetEventsAction.php index b9d17fec..c269285b 100644 --- a/backend/app/Http/Actions/Events/GetEventsAction.php +++ b/backend/app/Http/Actions/Events/GetEventsAction.php @@ -8,8 +8,8 @@ use HiEvents\DomainObjects\EventDomainObject; use HiEvents\Http\Actions\BaseAction; use HiEvents\Resources\Event\EventResource; -use HiEvents\Services\Handlers\Event\DTO\GetEventsDTO; -use HiEvents\Services\Handlers\Event\GetEventsHandler; +use HiEvents\Services\Application\Handlers\Event\DTO\GetEventsDTO; +use HiEvents\Services\Application\Handlers\Event\GetEventsHandler; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; diff --git a/backend/app/Http/Actions/Events/Images/CreateEventImageAction.php b/backend/app/Http/Actions/Events/Images/CreateEventImageAction.php index 9ec812cb..02b6e8e2 100644 --- a/backend/app/Http/Actions/Events/Images/CreateEventImageAction.php +++ b/backend/app/Http/Actions/Events/Images/CreateEventImageAction.php @@ -6,8 +6,8 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\Event\CreateEventImageRequest; use HiEvents\Resources\Image\ImageResource; -use HiEvents\Services\Handlers\Event\CreateEventImageHandler; -use HiEvents\Services\Handlers\Event\DTO\CreateEventImageDTO; +use HiEvents\Services\Application\Handlers\Event\CreateEventImageHandler; +use HiEvents\Services\Application\Handlers\Event\DTO\CreateEventImageDTO; use Illuminate\Http\JsonResponse; class CreateEventImageAction extends BaseAction diff --git a/backend/app/Http/Actions/Events/Images/DeleteEventImageAction.php b/backend/app/Http/Actions/Events/Images/DeleteEventImageAction.php index b6363985..53ad71e9 100644 --- a/backend/app/Http/Actions/Events/Images/DeleteEventImageAction.php +++ b/backend/app/Http/Actions/Events/Images/DeleteEventImageAction.php @@ -4,8 +4,8 @@ use HiEvents\DomainObjects\EventDomainObject; use HiEvents\Http\Actions\BaseAction; -use HiEvents\Services\Handlers\Event\DeleteEventImageHandler; -use HiEvents\Services\Handlers\Event\DTO\DeleteEventImageDTO; +use HiEvents\Services\Application\Handlers\Event\DeleteEventImageHandler; +use HiEvents\Services\Application\Handlers\Event\DTO\DeleteEventImageDTO; use Illuminate\Http\Response; class DeleteEventImageAction extends BaseAction diff --git a/backend/app/Http/Actions/Events/Stats/GetEventCheckInStatsAction.php b/backend/app/Http/Actions/Events/Stats/GetEventCheckInStatsAction.php index 0f8bbb91..24df05be 100644 --- a/backend/app/Http/Actions/Events/Stats/GetEventCheckInStatsAction.php +++ b/backend/app/Http/Actions/Events/Stats/GetEventCheckInStatsAction.php @@ -4,7 +4,7 @@ use HiEvents\DomainObjects\EventDomainObject; use HiEvents\Http\Actions\BaseAction; -use HiEvents\Services\Handlers\Event\GetEventCheckInStatsHandler; +use HiEvents\Services\Application\Handlers\Event\GetEventCheckInStatsHandler; use Illuminate\Http\JsonResponse; use Illuminate\Http\Resources\Json\JsonResource; diff --git a/backend/app/Http/Actions/Events/Stats/GetEventStatsAction.php b/backend/app/Http/Actions/Events/Stats/GetEventStatsAction.php index a2d7a4dc..bd42face 100644 --- a/backend/app/Http/Actions/Events/Stats/GetEventStatsAction.php +++ b/backend/app/Http/Actions/Events/Stats/GetEventStatsAction.php @@ -5,8 +5,8 @@ use Carbon\Carbon; use HiEvents\DomainObjects\EventDomainObject; use HiEvents\Http\Actions\BaseAction; -use HiEvents\Services\Handlers\Event\DTO\EventStatsRequestDTO; -use HiEvents\Services\Handlers\Event\GetEventStatsHandler; +use HiEvents\Services\Application\Handlers\Event\DTO\EventStatsRequestDTO; +use HiEvents\Services\Application\Handlers\Event\GetEventStatsHandler; use Illuminate\Http\JsonResponse; use Illuminate\Http\Resources\Json\JsonResource; diff --git a/backend/app/Http/Actions/Events/UpdateEventAction.php b/backend/app/Http/Actions/Events/UpdateEventAction.php index 17dcba07..87b2c788 100644 --- a/backend/app/Http/Actions/Events/UpdateEventAction.php +++ b/backend/app/Http/Actions/Events/UpdateEventAction.php @@ -7,8 +7,8 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\Event\UpdateEventRequest; use HiEvents\Resources\Event\EventResource; -use HiEvents\Services\Handlers\Event\DTO\UpdateEventDTO; -use HiEvents\Services\Handlers\Event\UpdateEventHandler; +use HiEvents\Services\Application\Handlers\Event\DTO\UpdateEventDTO; +use HiEvents\Services\Application\Handlers\Event\UpdateEventHandler; use Illuminate\Http\JsonResponse; use Illuminate\Validation\ValidationException; use Throwable; diff --git a/backend/app/Http/Actions/Events/UpdateEventStatusAction.php b/backend/app/Http/Actions/Events/UpdateEventStatusAction.php index a20ad150..0d33a45f 100644 --- a/backend/app/Http/Actions/Events/UpdateEventStatusAction.php +++ b/backend/app/Http/Actions/Events/UpdateEventStatusAction.php @@ -8,8 +8,8 @@ use HiEvents\Http\Request\Event\UpdateEventStatusRequest; use HiEvents\Http\ResponseCodes; use HiEvents\Resources\Event\EventResource; -use HiEvents\Services\Handlers\Event\DTO\UpdateEventStatusDTO; -use HiEvents\Services\Handlers\Event\UpdateEventStatusHandler; +use HiEvents\Services\Application\Handlers\Event\DTO\UpdateEventStatusDTO; +use HiEvents\Services\Application\Handlers\Event\UpdateEventStatusHandler; use Illuminate\Http\JsonResponse; class UpdateEventStatusAction extends BaseAction diff --git a/backend/app/Http/Actions/Messages/SendMessageAction.php b/backend/app/Http/Actions/Messages/SendMessageAction.php index 5bdb1910..04f7659c 100644 --- a/backend/app/Http/Actions/Messages/SendMessageAction.php +++ b/backend/app/Http/Actions/Messages/SendMessageAction.php @@ -7,8 +7,8 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\Message\SendMessageRequest; use HiEvents\Resources\Message\MessageResource; -use HiEvents\Services\Handlers\Message\DTO\SendMessageDTO; -use HiEvents\Services\Handlers\Message\SendMessageHandler; +use HiEvents\Services\Application\Handlers\Message\DTO\SendMessageDTO; +use HiEvents\Services\Application\Handlers\Message\SendMessageHandler; use Illuminate\Http\JsonResponse; use Symfony\Component\HttpFoundation\Response; @@ -36,7 +36,7 @@ public function __invoke(SendMessageRequest $request, int $eventId): JsonRespons 'is_test' => $request->input('is_test'), 'order_id' => $request->input('order_id'), 'attendee_ids' => $request->input('attendee_ids'), - 'ticket_ids' => $request->input('ticket_ids'), + 'product_ids' => $request->input('product_ids'), 'send_copy_to_current_user' => $request->boolean('send_copy_to_current_user'), 'sent_by_user_id' => $user->getId(), 'account_id' => $this->getAuthenticatedAccountId(), diff --git a/backend/app/Http/Actions/Orders/CancelOrderAction.php b/backend/app/Http/Actions/Orders/CancelOrderAction.php index ab98b514..d4853da9 100644 --- a/backend/app/Http/Actions/Orders/CancelOrderAction.php +++ b/backend/app/Http/Actions/Orders/CancelOrderAction.php @@ -7,8 +7,8 @@ use HiEvents\Exceptions\ResourceConflictException; use HiEvents\Http\Actions\BaseAction; use HiEvents\Resources\Order\OrderResource; -use HiEvents\Services\Handlers\Order\CancelOrderHandler; -use HiEvents\Services\Handlers\Order\DTO\CancelOrderDTO; +use HiEvents\Services\Application\Handlers\Order\CancelOrderHandler; +use HiEvents\Services\Application\Handlers\Order\DTO\CancelOrderDTO; use Illuminate\Http\JsonResponse; use Illuminate\Http\Response; use Symfony\Component\HttpFoundation\Response as HttpResponse; diff --git a/backend/app/Http/Actions/Orders/CompleteOrderActionPublic.php b/backend/app/Http/Actions/Orders/CompleteOrderActionPublic.php index 619c1dcb..db9459fd 100644 --- a/backend/app/Http/Actions/Orders/CompleteOrderActionPublic.php +++ b/backend/app/Http/Actions/Orders/CompleteOrderActionPublic.php @@ -8,9 +8,9 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\Order\CompleteOrderRequest; use HiEvents\Resources\Order\OrderResourcePublic; -use HiEvents\Services\Handlers\Order\CompleteOrderHandler; -use HiEvents\Services\Handlers\Order\DTO\CompleteOrderDTO; -use HiEvents\Services\Handlers\Order\DTO\CompleteOrderOrderDTO; +use HiEvents\Services\Application\Handlers\Order\CompleteOrderHandler; +use HiEvents\Services\Application\Handlers\Order\DTO\CompleteOrderDTO; +use HiEvents\Services\Application\Handlers\Order\DTO\CompleteOrderOrderDTO; use Illuminate\Http\JsonResponse; use Symfony\Component\HttpFoundation\Response; @@ -33,7 +33,7 @@ public function __invoke(CompleteOrderRequest $request, int $eventId, string $or ? $request->input('order.questions') : null, ]), - 'attendees' => $request->input('attendees'), + 'products' => $request->input('products'), ])); } catch (ResourceConflictException $e) { return $this->errorResponse($e->getMessage(), Response::HTTP_CONFLICT); diff --git a/backend/app/Http/Actions/Orders/CreateOrderActionPublic.php b/backend/app/Http/Actions/Orders/CreateOrderActionPublic.php index c789ae6c..52c93653 100644 --- a/backend/app/Http/Actions/Orders/CreateOrderActionPublic.php +++ b/backend/app/Http/Actions/Orders/CreateOrderActionPublic.php @@ -8,11 +8,11 @@ use HiEvents\Http\Request\Order\CreateOrderRequest; use HiEvents\Http\ResponseCodes; use HiEvents\Resources\Order\OrderResourcePublic; +use HiEvents\Services\Application\Handlers\Order\CreateOrderHandler; +use HiEvents\Services\Application\Handlers\Order\DTO\CreateOrderPublicDTO; +use HiEvents\Services\Application\Handlers\Order\DTO\ProductOrderDetailsDTO; use HiEvents\Services\Application\Locale\LocaleService; use HiEvents\Services\Domain\Order\OrderCreateRequestValidationService; -use HiEvents\Services\Handlers\Order\CreateOrderHandler; -use HiEvents\Services\Handlers\Order\DTO\CreateOrderPublicDTO; -use HiEvents\Services\Handlers\Order\DTO\TicketOrderDetailsDTO; use HiEvents\Services\Infrastructure\Session\CheckoutSessionManagementService; use Illuminate\Http\JsonResponse; use Throwable; @@ -42,7 +42,7 @@ public function __invoke(CreateOrderRequest $request, int $eventId): JsonRespons CreateOrderPublicDTO::fromArray([ 'is_user_authenticated' => $this->isUserAuthenticated(), 'promo_code' => $request->input('promo_code'), - 'tickets' => TicketOrderDetailsDTO::collectionFromArray($request->input('tickets')), + 'products' => ProductOrderDetailsDTO::collectionFromArray($request->input('products')), 'session_identifier' => $sessionId, 'order_locale' => $this->localeService->getLocaleOrDefault($request->getPreferredLanguage()), ]) diff --git a/backend/app/Http/Actions/Orders/GetOrderActionPublic.php b/backend/app/Http/Actions/Orders/GetOrderActionPublic.php index e516dae8..23f8ddce 100644 --- a/backend/app/Http/Actions/Orders/GetOrderActionPublic.php +++ b/backend/app/Http/Actions/Orders/GetOrderActionPublic.php @@ -4,8 +4,8 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Resources\Order\OrderResourcePublic; -use HiEvents\Services\Handlers\Order\DTO\GetOrderPublicDTO; -use HiEvents\Services\Handlers\Order\GetOrderPublicHandler; +use HiEvents\Services\Application\Handlers\Order\DTO\GetOrderPublicDTO; +use HiEvents\Services\Application\Handlers\Order\GetOrderPublicHandler; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; diff --git a/backend/app/Http/Actions/Orders/Payment/RefundOrderAction.php b/backend/app/Http/Actions/Orders/Payment/RefundOrderAction.php index 5efd5d2c..205409e2 100644 --- a/backend/app/Http/Actions/Orders/Payment/RefundOrderAction.php +++ b/backend/app/Http/Actions/Orders/Payment/RefundOrderAction.php @@ -7,8 +7,8 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\Order\RefundOrderRequest; use HiEvents\Resources\Order\OrderResource; -use HiEvents\Services\Handlers\Order\DTO\RefundOrderDTO; -use HiEvents\Services\Handlers\Order\Payment\Stripe\RefundOrderHandler; +use HiEvents\Services\Application\Handlers\Order\DTO\RefundOrderDTO; +use HiEvents\Services\Application\Handlers\Order\Payment\Stripe\RefundOrderHandler; use Illuminate\Http\JsonResponse; use Illuminate\Validation\ValidationException; use Stripe\Exception\ApiErrorException; diff --git a/backend/app/Http/Actions/Orders/Payment/Stripe/CreatePaymentIntentActionPublic.php b/backend/app/Http/Actions/Orders/Payment/Stripe/CreatePaymentIntentActionPublic.php index 06516697..1e3ee468 100644 --- a/backend/app/Http/Actions/Orders/Payment/Stripe/CreatePaymentIntentActionPublic.php +++ b/backend/app/Http/Actions/Orders/Payment/Stripe/CreatePaymentIntentActionPublic.php @@ -4,7 +4,7 @@ use HiEvents\Exceptions\Stripe\CreatePaymentIntentFailedException; use HiEvents\Http\Actions\BaseAction; -use HiEvents\Services\Handlers\Order\Payment\Stripe\CreatePaymentIntentHandler; +use HiEvents\Services\Application\Handlers\Order\Payment\Stripe\CreatePaymentIntentHandler; use Illuminate\Http\JsonResponse; use Symfony\Component\HttpFoundation\Response; diff --git a/backend/app/Http/Actions/Orders/Payment/Stripe/GetPaymentIntentActionPublic.php b/backend/app/Http/Actions/Orders/Payment/Stripe/GetPaymentIntentActionPublic.php index 05dd6bf7..3b5073c1 100644 --- a/backend/app/Http/Actions/Orders/Payment/Stripe/GetPaymentIntentActionPublic.php +++ b/backend/app/Http/Actions/Orders/Payment/Stripe/GetPaymentIntentActionPublic.php @@ -3,7 +3,7 @@ namespace HiEvents\Http\Actions\Orders\Payment\Stripe; use HiEvents\Http\Actions\BaseAction; -use HiEvents\Services\Handlers\Order\Payment\Stripe\GetPaymentIntentHandler; +use HiEvents\Services\Application\Handlers\Order\Payment\Stripe\GetPaymentIntentHandler; use Illuminate\Http\JsonResponse; class GetPaymentIntentActionPublic extends BaseAction diff --git a/backend/app/Http/Actions/Organizers/CreateOrganizerAction.php b/backend/app/Http/Actions/Organizers/CreateOrganizerAction.php index ba71304b..85448d0b 100644 --- a/backend/app/Http/Actions/Organizers/CreateOrganizerAction.php +++ b/backend/app/Http/Actions/Organizers/CreateOrganizerAction.php @@ -6,8 +6,8 @@ use HiEvents\Http\Request\Organizer\UpsertOrganizerRequest; use HiEvents\Http\ResponseCodes; use HiEvents\Resources\Organizer\OrganizerResource; -use HiEvents\Services\Handlers\Organizer\CreateOrganizerHandler; -use HiEvents\Services\Handlers\Organizer\DTO\CreateOrganizerDTO; +use HiEvents\Services\Application\Handlers\Organizer\CreateOrganizerHandler; +use HiEvents\Services\Application\Handlers\Organizer\DTO\CreateOrganizerDTO; use Illuminate\Http\JsonResponse; class CreateOrganizerAction extends BaseAction diff --git a/backend/app/Http/Actions/Organizers/EditOrganizerAction.php b/backend/app/Http/Actions/Organizers/EditOrganizerAction.php index 8e08c9e0..4aae07dc 100644 --- a/backend/app/Http/Actions/Organizers/EditOrganizerAction.php +++ b/backend/app/Http/Actions/Organizers/EditOrganizerAction.php @@ -6,8 +6,8 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\Organizer\UpsertOrganizerRequest; use HiEvents\Resources\Organizer\OrganizerResource; -use HiEvents\Services\Handlers\Organizer\DTO\EditOrganizerDTO; -use HiEvents\Services\Handlers\Organizer\EditOrganizerHandler; +use HiEvents\Services\Application\Handlers\Organizer\DTO\EditOrganizerDTO; +use HiEvents\Services\Application\Handlers\Organizer\EditOrganizerHandler; use Illuminate\Http\JsonResponse; class EditOrganizerAction extends BaseAction diff --git a/backend/app/Http/Actions/Organizers/GetOrganizerEventsAction.php b/backend/app/Http/Actions/Organizers/GetOrganizerEventsAction.php index 0f90d3c9..b5e8aed6 100644 --- a/backend/app/Http/Actions/Organizers/GetOrganizerEventsAction.php +++ b/backend/app/Http/Actions/Organizers/GetOrganizerEventsAction.php @@ -7,8 +7,8 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\DTO\QueryParamsDTO; use HiEvents\Resources\Event\EventResource; -use HiEvents\Services\Handlers\Organizer\DTO\GetOrganizerEventsDTO; -use HiEvents\Services\Handlers\Organizer\GetOrganizerEventsHandler; +use HiEvents\Services\Application\Handlers\Organizer\DTO\GetOrganizerEventsDTO; +use HiEvents\Services\Application\Handlers\Organizer\GetOrganizerEventsHandler; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; diff --git a/backend/app/Http/Actions/ProductCategories/CreateProductCategoryAction.php b/backend/app/Http/Actions/ProductCategories/CreateProductCategoryAction.php new file mode 100644 index 00000000..d3b84600 --- /dev/null +++ b/backend/app/Http/Actions/ProductCategories/CreateProductCategoryAction.php @@ -0,0 +1,40 @@ +isActionAuthorized($eventId, EventDomainObject::class); + + $productCategory = $this->handler->handle(new UpsertProductCategoryDTO( + name: $request->validated('name'), + description: $request->validated('description'), + is_hidden: $request->validated('is_hidden'), + event_id: $eventId, + no_products_message: $request->validated('no_products_message'), + )); + + return $this->resourceResponse( + resource: ProductCategoryResource::class, + data: $productCategory, + statusCode: ResponseCodes::HTTP_CREATED, + ); + } +} diff --git a/backend/app/Http/Actions/ProductCategories/DeleteProductCategoryAction.php b/backend/app/Http/Actions/ProductCategories/DeleteProductCategoryAction.php new file mode 100644 index 00000000..aba5e2f0 --- /dev/null +++ b/backend/app/Http/Actions/ProductCategories/DeleteProductCategoryAction.php @@ -0,0 +1,46 @@ +isActionAuthorized($eventId, EventDomainObject::class); + + try { + $this->deleteProductCategoryHandler->handle( + productCategoryId: $productCategoryId, + eventId: $eventId, + ); + } catch (CannotDeleteEntityException $exception) { + return $this->errorResponse( + message: $exception->getMessage(), + statusCode: Response::HTTP_CONFLICT, + ); + } + + return $this->deletedResponse(); + } +} diff --git a/backend/app/Http/Actions/ProductCategories/EditProductCategoryAction.php b/backend/app/Http/Actions/ProductCategories/EditProductCategoryAction.php new file mode 100644 index 00000000..56a64fb2 --- /dev/null +++ b/backend/app/Http/Actions/ProductCategories/EditProductCategoryAction.php @@ -0,0 +1,42 @@ +isActionAuthorized($eventId, EventDomainObject::class); + + $request->merge([ + 'event_id' => $eventId, + 'account_id' => $this->getAuthenticatedAccountId(), + 'product_category_id' => $productCategoryId, + ]); + + $productCategory = $this->editProductCategoryHandler->handle(new UpsertProductCategoryDTO( + name: $request->validated('name'), + description: $request->validated('description'), + is_hidden: $request->validated('is_hidden'), + event_id: $eventId, + no_products_message: $request->validated('no_products_message'), + product_category_id: $productCategoryId, + )); + + return $this->resourceResponse(ProductCategoryResource::class, $productCategory); + } +} diff --git a/backend/app/Http/Actions/ProductCategories/GetProductCategoriesAction.php b/backend/app/Http/Actions/ProductCategories/GetProductCategoriesAction.php new file mode 100644 index 00000000..daf9512e --- /dev/null +++ b/backend/app/Http/Actions/ProductCategories/GetProductCategoriesAction.php @@ -0,0 +1,30 @@ +isActionAuthorized($eventId, EventDomainObject::class); + + $categories = $this->getProductCategoriesHandler->handle($eventId); + + return $this->resourceResponse( + resource: ProductCategoryResource::class, + data: $categories, + ); + } +} diff --git a/backend/app/Http/Actions/ProductCategories/GetProductCategoryAction.php b/backend/app/Http/Actions/ProductCategories/GetProductCategoryAction.php new file mode 100644 index 00000000..ddec4dd5 --- /dev/null +++ b/backend/app/Http/Actions/ProductCategories/GetProductCategoryAction.php @@ -0,0 +1,30 @@ +isActionAuthorized($eventId, EventDomainObject::class); + + $category = $this->getProductCategoryHandler->handle($eventId, $productCategoryId); + + return $this->resourceResponse( + resource: ProductCategoryResource::class, + data: $category, + ); + } +} diff --git a/backend/app/Http/Actions/Tickets/CreateTicketAction.php b/backend/app/Http/Actions/Products/CreateProductAction.php similarity index 53% rename from backend/app/Http/Actions/Tickets/CreateTicketAction.php rename to backend/app/Http/Actions/Products/CreateProductAction.php index 7340b99b..905474b9 100644 --- a/backend/app/Http/Actions/Tickets/CreateTicketAction.php +++ b/backend/app/Http/Actions/Products/CreateProductAction.php @@ -2,33 +2,33 @@ declare(strict_types=1); -namespace HiEvents\Http\Actions\Tickets; +namespace HiEvents\Http\Actions\Products; use HiEvents\DomainObjects\EventDomainObject; use HiEvents\Exceptions\InvalidTaxOrFeeIdException; use HiEvents\Http\Actions\BaseAction; -use HiEvents\Http\Request\Ticket\UpsertTicketRequest; +use HiEvents\Http\Request\Product\UpsertProductRequest; use HiEvents\Http\ResponseCodes; -use HiEvents\Resources\Ticket\TicketResource; -use HiEvents\Services\Handlers\Ticket\CreateTicketHandler; -use HiEvents\Services\Handlers\Ticket\DTO\UpsertTicketDTO; +use HiEvents\Resources\Product\ProductResource; +use HiEvents\Services\Application\Handlers\Product\CreateProductHandler; +use HiEvents\Services\Application\Handlers\Product\DTO\UpsertProductDTO; use Illuminate\Http\JsonResponse; use Illuminate\Validation\ValidationException; use Throwable; -class CreateTicketAction extends BaseAction +class CreateProductAction extends BaseAction { - private CreateTicketHandler $createTicketHandler; + private CreateProductHandler $createProductHandler; - public function __construct(CreateTicketHandler $handler) + public function __construct(CreateProductHandler $handler) { - $this->createTicketHandler = $handler; + $this->createProductHandler = $handler; } /** * @throws Throwable */ - public function __invoke(int $eventId, UpsertTicketRequest $request): JsonResponse + public function __invoke(int $eventId, UpsertProductRequest $request): JsonResponse { $this->isActionAuthorized($eventId, EventDomainObject::class); @@ -38,7 +38,7 @@ public function __invoke(int $eventId, UpsertTicketRequest $request): JsonRespon ]); try { - $ticket = $this->createTicketHandler->handle(UpsertTicketDTO::fromArray($request->all())); + $product = $this->createProductHandler->handle(UpsertProductDTO::fromArray($request->all())); } catch (InvalidTaxOrFeeIdException $e) { throw ValidationException::withMessages([ 'tax_and_fee_ids' => $e->getMessage(), @@ -46,8 +46,8 @@ public function __invoke(int $eventId, UpsertTicketRequest $request): JsonRespon } return $this->resourceResponse( - resource: TicketResource::class, - data: $ticket, + resource: ProductResource::class, + data: $product, statusCode: ResponseCodes::HTTP_CREATED, ); } diff --git a/backend/app/Http/Actions/Tickets/DeleteTicketAction.php b/backend/app/Http/Actions/Products/DeleteProductAction.php similarity index 59% rename from backend/app/Http/Actions/Tickets/DeleteTicketAction.php rename to backend/app/Http/Actions/Products/DeleteProductAction.php index 7c51cad3..9a34a6c7 100644 --- a/backend/app/Http/Actions/Tickets/DeleteTicketAction.php +++ b/backend/app/Http/Actions/Products/DeleteProductAction.php @@ -2,32 +2,32 @@ declare(strict_types=1); -namespace HiEvents\Http\Actions\Tickets; +namespace HiEvents\Http\Actions\Products; use HiEvents\DomainObjects\EventDomainObject; use HiEvents\Exceptions\CannotDeleteEntityException; use HiEvents\Http\Actions\BaseAction; -use HiEvents\Services\Handlers\Ticket\DeleteTicketHandler; +use HiEvents\Services\Application\Handlers\Product\DeleteProductHandler; use Illuminate\Http\JsonResponse; use Illuminate\Http\Response; use Symfony\Component\HttpFoundation\Response as HttpResponse; -class DeleteTicketAction extends BaseAction +class DeleteProductAction extends BaseAction { - private DeleteTicketHandler $deleteTicketHandler; + private DeleteProductHandler $deleteProductHandler; - public function __construct(DeleteTicketHandler $handler) + public function __construct(DeleteProductHandler $handler) { - $this->deleteTicketHandler = $handler; + $this->deleteProductHandler = $handler; } - public function __invoke(int $eventId, int $ticketId): Response|JsonResponse + public function __invoke(int $eventId, int $productId): Response|JsonResponse { $this->isActionAuthorized($eventId, EventDomainObject::class); try { - $this->deleteTicketHandler->handle( - ticketId: $ticketId, + $this->deleteProductHandler->handle( + productId: $productId, eventId: $eventId, ); } catch (CannotDeleteEntityException $exception) { diff --git a/backend/app/Http/Actions/Tickets/EditTicketAction.php b/backend/app/Http/Actions/Products/EditProductAction.php similarity index 52% rename from backend/app/Http/Actions/Tickets/EditTicketAction.php rename to backend/app/Http/Actions/Products/EditProductAction.php index d03111e8..c9f79d1c 100644 --- a/backend/app/Http/Actions/Tickets/EditTicketAction.php +++ b/backend/app/Http/Actions/Products/EditProductAction.php @@ -2,24 +2,24 @@ declare(strict_types=1); -namespace HiEvents\Http\Actions\Tickets; +namespace HiEvents\Http\Actions\Products; use HiEvents\DomainObjects\EventDomainObject; -use HiEvents\Exceptions\CannotChangeTicketTypeException; +use HiEvents\Exceptions\CannotChangeProductTypeException; use HiEvents\Exceptions\InvalidTaxOrFeeIdException; use HiEvents\Http\Actions\BaseAction; -use HiEvents\Http\Request\Ticket\UpsertTicketRequest; -use HiEvents\Resources\Ticket\TicketResource; -use HiEvents\Services\Handlers\Ticket\DTO\UpsertTicketDTO; -use HiEvents\Services\Handlers\Ticket\EditTicketHandler; +use HiEvents\Http\Request\Product\UpsertProductRequest; +use HiEvents\Resources\Product\ProductResource; +use HiEvents\Services\Application\Handlers\Product\DTO\UpsertProductDTO; +use HiEvents\Services\Application\Handlers\Product\EditProductHandler; use Illuminate\Http\JsonResponse; use Illuminate\Validation\ValidationException; use Throwable; -class EditTicketAction extends BaseAction +class EditProductAction extends BaseAction { public function __construct( - private readonly EditTicketHandler $editTicketHandler, + private readonly EditProductHandler $editProductHandler, ) { } @@ -28,28 +28,28 @@ public function __construct( * @throws Throwable * @throws ValidationException */ - public function __invoke(UpsertTicketRequest $request, int $eventId, int $ticketId): JsonResponse + public function __invoke(UpsertProductRequest $request, int $eventId, int $productId): JsonResponse { $this->isActionAuthorized($eventId, EventDomainObject::class); $request->merge([ 'event_id' => $eventId, 'account_id' => $this->getAuthenticatedAccountId(), - 'ticket_id' => $ticketId, + 'product_id' => $productId, ]); try { - $ticket = $this->editTicketHandler->handle(UpsertTicketDTO::fromArray($request->all())); + $product = $this->editProductHandler->handle(UpsertProductDTO::fromArray($request->all())); } catch (InvalidTaxOrFeeIdException $e) { throw ValidationException::withMessages([ 'tax_and_fee_ids' => $e->getMessage(), ]); - } catch (CannotChangeTicketTypeException $e) { + } catch (CannotChangeProductTypeException $e) { throw ValidationException::withMessages([ 'type' => $e->getMessage(), ]); } - return $this->resourceResponse(TicketResource::class, $ticket); + return $this->resourceResponse(ProductResource::class, $product); } } diff --git a/backend/app/Http/Actions/Products/GetProductAction.php b/backend/app/Http/Actions/Products/GetProductAction.php new file mode 100644 index 00000000..f73ec847 --- /dev/null +++ b/backend/app/Http/Actions/Products/GetProductAction.php @@ -0,0 +1,37 @@ +productRepository = $productRepository; + } + + public function __invoke(int $eventId, int $productId): JsonResponse + { + $this->isActionAuthorized($eventId, EventDomainObject::class); + + return $this->resourceResponse(ProductResource::class, $this->productRepository + ->loadRelation(TaxAndFeesDomainObject::class) + ->loadRelation(ProductPriceDomainObject::class) + ->findFirstWhere([ + ProductDomainObjectAbstract::EVENT_ID => $eventId, + ProductDomainObjectAbstract::ID => $productId, + ])); + } +} diff --git a/backend/app/Http/Actions/Tickets/GetTicketsAction.php b/backend/app/Http/Actions/Products/GetProductsAction.php similarity index 53% rename from backend/app/Http/Actions/Tickets/GetTicketsAction.php rename to backend/app/Http/Actions/Products/GetProductsAction.php index e962de26..d71e5e1c 100644 --- a/backend/app/Http/Actions/Tickets/GetTicketsAction.php +++ b/backend/app/Http/Actions/Products/GetProductsAction.php @@ -2,20 +2,20 @@ declare(strict_types=1); -namespace HiEvents\Http\Actions\Tickets; +namespace HiEvents\Http\Actions\Products; use HiEvents\DomainObjects\EventDomainObject; -use HiEvents\DomainObjects\TicketDomainObject; +use HiEvents\DomainObjects\ProductDomainObject; use HiEvents\Http\Actions\BaseAction; -use HiEvents\Resources\Ticket\TicketResource; -use HiEvents\Services\Handlers\Ticket\GetTicketsHandler; +use HiEvents\Resources\Product\ProductResource; +use HiEvents\Services\Application\Handlers\Product\GetProductsHandler; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -class GetTicketsAction extends BaseAction +class GetProductsAction extends BaseAction { public function __construct( - private readonly GetTicketsHandler $getTicketsHandler, + private readonly GetProductsHandler $getProductsHandler, ) { } @@ -24,15 +24,15 @@ public function __invoke(int $eventId, Request $request): JsonResponse { $this->isActionAuthorized($eventId, EventDomainObject::class); - $tickets = $this->getTicketsHandler->handle( + $products = $this->getProductsHandler->handle( eventId: $eventId, queryParamsDTO: $this->getPaginationQueryParams($request), ); return $this->filterableResourceResponse( - resource: TicketResource::class, - data: $tickets, - domainObject: TicketDomainObject::class + resource: ProductResource::class, + data: $products, + domainObject: ProductDomainObject::class ); } } diff --git a/backend/app/Http/Actions/Tickets/SortTicketsAction.php b/backend/app/Http/Actions/Products/SortProductsAction.php similarity index 55% rename from backend/app/Http/Actions/Tickets/SortTicketsAction.php rename to backend/app/Http/Actions/Products/SortProductsAction.php index 1e177757..ea5f4504 100644 --- a/backend/app/Http/Actions/Tickets/SortTicketsAction.php +++ b/backend/app/Http/Actions/Products/SortProductsAction.php @@ -1,31 +1,31 @@ isActionAuthorized($eventId, EventDomainObject::class); try { - $this->sortTicketsHandler->handle( + $this->sortProductsHandler->handle( $eventId, - $request->validated(), + $request->validated('sorted_categories'), ); } catch (ResourceConflictException $e) { return $this->errorResponse($e->getMessage(), Response::HTTP_CONFLICT); diff --git a/backend/app/Http/Actions/PromoCodes/CreatePromoCodeAction.php b/backend/app/Http/Actions/PromoCodes/CreatePromoCodeAction.php index f1c8b8b7..0e2bc37f 100644 --- a/backend/app/Http/Actions/PromoCodes/CreatePromoCodeAction.php +++ b/backend/app/Http/Actions/PromoCodes/CreatePromoCodeAction.php @@ -9,9 +9,9 @@ use HiEvents\Http\Request\PromoCode\CreateUpdatePromoCodeRequest; use HiEvents\Http\ResponseCodes; use HiEvents\Resources\PromoCode\PromoCodeResource; -use HiEvents\Services\Domain\Ticket\Exception\UnrecognizedTicketIdException; -use HiEvents\Services\Handlers\PromoCode\CreatePromoCodeHandler; -use HiEvents\Services\Handlers\PromoCode\DTO\UpsertPromoCodeDTO; +use HiEvents\Services\Application\Handlers\PromoCode\CreatePromoCodeHandler; +use HiEvents\Services\Application\Handlers\PromoCode\DTO\UpsertPromoCodeDTO; +use HiEvents\Services\Domain\Product\Exception\UnrecognizedProductIdException; use Illuminate\Http\JsonResponse; use Illuminate\Validation\ValidationException; @@ -35,7 +35,7 @@ public function __invoke(CreateUpdatePromoCodeRequest $request, int $eventId): J $promoCode = $this->createPromoCodeHandler->handle($eventId, new UpsertPromoCodeDTO( code: strtolower($request->input('code')), event_id: $eventId, - applicable_ticket_ids: $request->input('applicable_ticket_ids'), + applicable_product_ids: $request->input('applicable_product_ids'), discount_type: PromoCodeDiscountTypeEnum::fromName($request->input('discount_type')), discount: $request->float('discount'), expiry_date: $request->input('expiry_date'), @@ -45,9 +45,9 @@ public function __invoke(CreateUpdatePromoCodeRequest $request, int $eventId): J throw ValidationException::withMessages([ 'code' => $e->getMessage(), ]); - } catch (UnrecognizedTicketIdException $e) { + } catch (UnrecognizedProductIdException $e) { throw ValidationException::withMessages([ - 'applicable_ticket_ids' => $e->getMessage(), + 'applicable_product_ids' => $e->getMessage(), ]); } diff --git a/backend/app/Http/Actions/PromoCodes/DeletePromoCodeAction.php b/backend/app/Http/Actions/PromoCodes/DeletePromoCodeAction.php index ae61f4e6..f6bc2be6 100644 --- a/backend/app/Http/Actions/PromoCodes/DeletePromoCodeAction.php +++ b/backend/app/Http/Actions/PromoCodes/DeletePromoCodeAction.php @@ -4,8 +4,8 @@ use HiEvents\DomainObjects\EventDomainObject; use HiEvents\Http\Actions\BaseAction; -use HiEvents\Services\Handlers\PromoCode\DeletePromoCodeHandler; -use HiEvents\Services\Handlers\PromoCode\DTO\DeletePromoCodeDTO; +use HiEvents\Services\Application\Handlers\PromoCode\DeletePromoCodeHandler; +use HiEvents\Services\Application\Handlers\PromoCode\DTO\DeletePromoCodeDTO; use Illuminate\Http\Request; use Illuminate\Http\Response; diff --git a/backend/app/Http/Actions/PromoCodes/UpdatePromoCodeAction.php b/backend/app/Http/Actions/PromoCodes/UpdatePromoCodeAction.php index 0416ed80..d9ea2109 100644 --- a/backend/app/Http/Actions/PromoCodes/UpdatePromoCodeAction.php +++ b/backend/app/Http/Actions/PromoCodes/UpdatePromoCodeAction.php @@ -9,9 +9,9 @@ use HiEvents\Http\Request\PromoCode\CreateUpdatePromoCodeRequest; use HiEvents\Http\ResponseCodes; use HiEvents\Resources\PromoCode\PromoCodeResource; -use HiEvents\Services\Domain\Ticket\Exception\UnrecognizedTicketIdException; -use HiEvents\Services\Handlers\PromoCode\DTO\UpsertPromoCodeDTO; -use HiEvents\Services\Handlers\PromoCode\UpdatePromoCodeHandler; +use HiEvents\Services\Application\Handlers\PromoCode\DTO\UpsertPromoCodeDTO; +use HiEvents\Services\Application\Handlers\PromoCode\UpdatePromoCodeHandler; +use HiEvents\Services\Domain\Product\Exception\UnrecognizedProductIdException; use Illuminate\Http\JsonResponse; use Illuminate\Validation\ValidationException; @@ -35,7 +35,7 @@ public function __invoke(CreateUpdatePromoCodeRequest $request, int $eventId, in $promoCode = $this->updatePromoCodeHandler->handle($promoCodeId, new UpsertPromoCodeDTO( code: strtolower($request->input('code')), event_id: $eventId, - applicable_ticket_ids: $request->input('applicable_ticket_ids'), + applicable_product_ids: $request->input('applicable_product_ids'), discount_type: PromoCodeDiscountTypeEnum::fromName($request->input('discount_type')), discount: $request->float('discount'), expiry_date: $request->input('expiry_date'), @@ -45,9 +45,9 @@ public function __invoke(CreateUpdatePromoCodeRequest $request, int $eventId, in throw ValidationException::withMessages([ 'code' => $e->getMessage(), ]); - } catch (UnrecognizedTicketIdException $e) { + } catch (UnrecognizedProductIdException $e) { throw ValidationException::withMessages([ - 'applicable_ticket_ids' => $e->getMessage(), + 'applicable_product_ids' => $e->getMessage(), ]); } diff --git a/backend/app/Http/Actions/Questions/CreateQuestionAction.php b/backend/app/Http/Actions/Questions/CreateQuestionAction.php index a5a9ca71..7c65efc1 100644 --- a/backend/app/Http/Actions/Questions/CreateQuestionAction.php +++ b/backend/app/Http/Actions/Questions/CreateQuestionAction.php @@ -7,8 +7,8 @@ use HiEvents\Http\Request\Questions\UpsertQuestionRequest; use HiEvents\Http\ResponseCodes; use HiEvents\Resources\Question\QuestionResource; -use HiEvents\Services\Handlers\Question\CreateQuestionHandler; -use HiEvents\Services\Handlers\Question\DTO\UpsertQuestionDTO; +use HiEvents\Services\Application\Handlers\Question\CreateQuestionHandler; +use HiEvents\Services\Application\Handlers\Question\DTO\UpsertQuestionDTO; use Illuminate\Http\JsonResponse; class CreateQuestionAction extends BaseAction @@ -30,7 +30,7 @@ public function __invoke(UpsertQuestionRequest $request, int $eventId): JsonResp 'required' => $request->boolean('required'), 'options' => $request->input('options'), 'event_id' => $eventId, - 'ticket_ids' => $request->input('ticket_ids'), + 'product_ids' => $request->input('product_ids'), 'belongs_to' => $request->input('belongs_to'), 'is_hidden' => $request->boolean('is_hidden'), 'description' => $request->input('description'), diff --git a/backend/app/Http/Actions/Questions/DeleteQuestionAction.php b/backend/app/Http/Actions/Questions/DeleteQuestionAction.php index e30bbc17..1b786926 100644 --- a/backend/app/Http/Actions/Questions/DeleteQuestionAction.php +++ b/backend/app/Http/Actions/Questions/DeleteQuestionAction.php @@ -5,7 +5,7 @@ use HiEvents\DomainObjects\EventDomainObject; use HiEvents\Exceptions\CannotDeleteEntityException; use HiEvents\Http\Actions\BaseAction; -use HiEvents\Services\Handlers\Question\DeleteQuestionHandler; +use HiEvents\Services\Application\Handlers\Question\DeleteQuestionHandler; use Illuminate\Http\JsonResponse; use Illuminate\Http\Response; use Throwable; diff --git a/backend/app/Http/Actions/Questions/EditQuestionAction.php b/backend/app/Http/Actions/Questions/EditQuestionAction.php index 616584e9..847f4d01 100644 --- a/backend/app/Http/Actions/Questions/EditQuestionAction.php +++ b/backend/app/Http/Actions/Questions/EditQuestionAction.php @@ -8,8 +8,8 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\Questions\UpsertQuestionRequest; use HiEvents\Resources\Question\QuestionResource; -use HiEvents\Services\Handlers\Question\DTO\UpsertQuestionDTO; -use HiEvents\Services\Handlers\Question\EditQuestionHandler; +use HiEvents\Services\Application\Handlers\Question\DTO\UpsertQuestionDTO; +use HiEvents\Services\Application\Handlers\Question\EditQuestionHandler; use Illuminate\Http\JsonResponse; use Throwable; @@ -37,7 +37,7 @@ public function __invoke(UpsertQuestionRequest $request, int $eventId, int $ques 'required' => $request->boolean('required'), 'options' => $request->input('options'), 'event_id' => $eventId, - 'ticket_ids' => $request->input('ticket_ids'), + 'product_ids' => $request->input('product_ids'), 'is_hidden' => $request->boolean('is_hidden'), 'belongs_to' => QuestionBelongsTo::fromName($request->input('belongs_to')), 'description' => $request->input('description'), diff --git a/backend/app/Http/Actions/Questions/GetQuestionAction.php b/backend/app/Http/Actions/Questions/GetQuestionAction.php index 6759fd3d..6701e7ce 100644 --- a/backend/app/Http/Actions/Questions/GetQuestionAction.php +++ b/backend/app/Http/Actions/Questions/GetQuestionAction.php @@ -3,7 +3,7 @@ namespace HiEvents\Http\Actions\Questions; use HiEvents\DomainObjects\EventDomainObject; -use HiEvents\DomainObjects\TicketDomainObject; +use HiEvents\DomainObjects\ProductDomainObject; use HiEvents\Http\Actions\BaseAction; use HiEvents\Repository\Interfaces\QuestionRepositoryInterface; use HiEvents\Resources\Question\QuestionResource; @@ -24,7 +24,7 @@ public function __invoke(Request $request, int $eventId, int $questionId): JsonR $this->isActionAuthorized($eventId, EventDomainObject::class); $questions = $this->questionRepository - ->loadRelation(TicketDomainObject::class) + ->loadRelation(ProductDomainObject::class) ->findById($questionId); return $this->resourceResponse(QuestionResource::class, $questions); diff --git a/backend/app/Http/Actions/Questions/GetQuestionsAction.php b/backend/app/Http/Actions/Questions/GetQuestionsAction.php index fa9fe3a4..4a234442 100644 --- a/backend/app/Http/Actions/Questions/GetQuestionsAction.php +++ b/backend/app/Http/Actions/Questions/GetQuestionsAction.php @@ -3,8 +3,8 @@ namespace HiEvents\Http\Actions\Questions; use HiEvents\DomainObjects\EventDomainObject; -use HiEvents\DomainObjects\TicketDomainObject; -use HiEvents\DomainObjects\TicketPriceDomainObject; +use HiEvents\DomainObjects\ProductDomainObject; +use HiEvents\DomainObjects\ProductPriceDomainObject; use HiEvents\Http\Actions\BaseAction; use HiEvents\Repository\Eloquent\Value\Relationship; use HiEvents\Repository\Interfaces\QuestionRepositoryInterface; @@ -27,8 +27,8 @@ public function __invoke(Request $request, int $eventId): JsonResponse $questions = $this->questionRepository ->loadRelation( - new Relationship(TicketDomainObject::class, [ - new Relationship(TicketPriceDomainObject::class) + new Relationship(ProductDomainObject::class, [ + new Relationship(ProductPriceDomainObject::class) ]) ) ->findByEventId($eventId); diff --git a/backend/app/Http/Actions/Questions/GetQuestionsPublicAction.php b/backend/app/Http/Actions/Questions/GetQuestionsPublicAction.php index 8fe82502..8e718f28 100644 --- a/backend/app/Http/Actions/Questions/GetQuestionsPublicAction.php +++ b/backend/app/Http/Actions/Questions/GetQuestionsPublicAction.php @@ -3,7 +3,7 @@ namespace HiEvents\Http\Actions\Questions; use HiEvents\DomainObjects\Generated\QuestionDomainObjectAbstract; -use HiEvents\DomainObjects\TicketDomainObject; +use HiEvents\DomainObjects\ProductDomainObject; use HiEvents\Http\Actions\BaseAction; use HiEvents\Repository\Interfaces\QuestionRepositoryInterface; use HiEvents\Resources\Question\QuestionResourcePublic; @@ -22,7 +22,7 @@ public function __construct(QuestionRepositoryInterface $questionRepository) public function __invoke(Request $request, int $eventId): JsonResponse { $questions = $this->questionRepository - ->loadRelation(TicketDomainObject::class) + ->loadRelation(ProductDomainObject::class) ->findWhere([ QuestionDomainObjectAbstract::EVENT_ID => $eventId, QuestionDomainObjectAbstract::IS_HIDDEN => false, diff --git a/backend/app/Http/Actions/Questions/SortQuestionsAction.php b/backend/app/Http/Actions/Questions/SortQuestionsAction.php index 0089ffc2..2394f3de 100644 --- a/backend/app/Http/Actions/Questions/SortQuestionsAction.php +++ b/backend/app/Http/Actions/Questions/SortQuestionsAction.php @@ -5,7 +5,7 @@ use HiEvents\DomainObjects\EventDomainObject; use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\Questions\SortQuestionsRequest; -use HiEvents\Services\Handlers\Question\SortQuestionsHandler; +use HiEvents\Services\Application\Handlers\Question\SortQuestionsHandler; use Illuminate\Http\JsonResponse; use Illuminate\Http\Response; use Symfony\Component\Routing\Exception\ResourceNotFoundException; diff --git a/backend/app/Http/Actions/Reports/GetReportAction.php b/backend/app/Http/Actions/Reports/GetReportAction.php new file mode 100644 index 00000000..5fa1596a --- /dev/null +++ b/backend/app/Http/Actions/Reports/GetReportAction.php @@ -0,0 +1,61 @@ +isActionAuthorized($eventId, EventDomainObject::class); + + $this->validateDateRange($request); + + if (!in_array($reportType, ReportTypes::valuesArray(), true)) { + throw new BadRequestHttpException('Invalid report type.'); + } + + $reportData = $this->reportHandler->handle( + reportData: new GetReportDTO( + eventId: $eventId, + reportType: ReportTypes::from($reportType), + startDate: $request->validated('start_date'), + endDate: $request->validated('end_date'), + ), + ); + + return $this->jsonResponse($reportData); + } + + /** + * @throws ValidationException + */ + private function validateDateRange(GetReportRequest $request): void + { + $startDate = $request->validated('start_date'); + $endDate = $request->validated('end_date'); + + $diffInDays = Carbon::parse($startDate)->diffInDays(Carbon::parse($endDate)); + + if ($diffInDays > 370) { + throw ValidationException::withMessages(['start_date' => 'Date range must be less than 370 days.']); + } + } +} diff --git a/backend/app/Http/Actions/TaxesAndFees/CreateTaxOrFeeAction.php b/backend/app/Http/Actions/TaxesAndFees/CreateTaxOrFeeAction.php index 77016a4a..228615d5 100644 --- a/backend/app/Http/Actions/TaxesAndFees/CreateTaxOrFeeAction.php +++ b/backend/app/Http/Actions/TaxesAndFees/CreateTaxOrFeeAction.php @@ -7,8 +7,8 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\TaxOrFee\CreateTaxOrFeeRequest; use HiEvents\Resources\Tax\TaxAndFeeResource; -use HiEvents\Services\Handlers\TaxAndFee\CreateTaxOrFeeHandler; -use HiEvents\Services\Handlers\TaxAndFee\DTO\UpsertTaxDTO; +use HiEvents\Services\Application\Handlers\TaxAndFee\CreateTaxOrFeeHandler; +use HiEvents\Services\Application\Handlers\TaxAndFee\DTO\UpsertTaxDTO; use Illuminate\Http\JsonResponse; use Illuminate\Validation\ValidationException; diff --git a/backend/app/Http/Actions/TaxesAndFees/DeleteTaxOrFeeAction.php b/backend/app/Http/Actions/TaxesAndFees/DeleteTaxOrFeeAction.php index 8bb4fe4e..55894381 100644 --- a/backend/app/Http/Actions/TaxesAndFees/DeleteTaxOrFeeAction.php +++ b/backend/app/Http/Actions/TaxesAndFees/DeleteTaxOrFeeAction.php @@ -5,8 +5,8 @@ use HiEvents\DomainObjects\TaxAndFeesDomainObject; use HiEvents\Exceptions\ResourceConflictException; use HiEvents\Http\Actions\BaseAction; -use HiEvents\Services\Handlers\TaxAndFee\DeleteTaxHandler; -use HiEvents\Services\Handlers\TaxAndFee\DTO\DeleteTaxDTO; +use HiEvents\Services\Application\Handlers\TaxAndFee\DeleteTaxHandler; +use HiEvents\Services\Application\Handlers\TaxAndFee\DTO\DeleteTaxDTO; use Illuminate\Http\Response; use Throwable; diff --git a/backend/app/Http/Actions/TaxesAndFees/EditTaxOrFeeAction.php b/backend/app/Http/Actions/TaxesAndFees/EditTaxOrFeeAction.php index e6957bd8..2aeaa2fe 100644 --- a/backend/app/Http/Actions/TaxesAndFees/EditTaxOrFeeAction.php +++ b/backend/app/Http/Actions/TaxesAndFees/EditTaxOrFeeAction.php @@ -7,8 +7,8 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\TaxOrFee\CreateTaxOrFeeRequest; use HiEvents\Resources\Tax\TaxAndFeeResource; -use HiEvents\Services\Handlers\TaxAndFee\DTO\UpsertTaxDTO; -use HiEvents\Services\Handlers\TaxAndFee\EditTaxHandler; +use HiEvents\Services\Application\Handlers\TaxAndFee\DTO\UpsertTaxDTO; +use HiEvents\Services\Application\Handlers\TaxAndFee\EditTaxHandler; use Illuminate\Http\JsonResponse; use Illuminate\Validation\ValidationException; diff --git a/backend/app/Http/Actions/Tickets/GetTicketAction.php b/backend/app/Http/Actions/Tickets/GetTicketAction.php deleted file mode 100644 index be28a9f4..00000000 --- a/backend/app/Http/Actions/Tickets/GetTicketAction.php +++ /dev/null @@ -1,37 +0,0 @@ -ticketRepository = $ticketRepository; - } - - public function __invoke(int $eventId, int $ticketId): JsonResponse - { - $this->isActionAuthorized($eventId, EventDomainObject::class); - - return $this->resourceResponse(TicketResource::class, $this->ticketRepository - ->loadRelation(TaxAndFeesDomainObject::class) - ->loadRelation(TicketPriceDomainObject::class) - ->findFirstWhere([ - TicketDomainObjectAbstract::EVENT_ID => $eventId, - TicketDomainObjectAbstract::ID => $ticketId, - ])); - } -} diff --git a/backend/app/Http/Actions/Users/CancelEmailChangeAction.php b/backend/app/Http/Actions/Users/CancelEmailChangeAction.php index 13451971..6675abff 100644 --- a/backend/app/Http/Actions/Users/CancelEmailChangeAction.php +++ b/backend/app/Http/Actions/Users/CancelEmailChangeAction.php @@ -5,8 +5,8 @@ use HiEvents\DomainObjects\UserDomainObject; use HiEvents\Http\Actions\BaseAction; use HiEvents\Resources\User\UserResource; -use HiEvents\Services\Handlers\User\CancelEmailChangeHandler; -use HiEvents\Services\Handlers\User\DTO\CancelEmailChangeDTO; +use HiEvents\Services\Application\Handlers\User\CancelEmailChangeHandler; +use HiEvents\Services\Application\Handlers\User\DTO\CancelEmailChangeDTO; use Illuminate\Http\JsonResponse; class CancelEmailChangeAction extends BaseAction diff --git a/backend/app/Http/Actions/Users/ConfirmEmailAddressAction.php b/backend/app/Http/Actions/Users/ConfirmEmailAddressAction.php index 28666b75..86c59e88 100644 --- a/backend/app/Http/Actions/Users/ConfirmEmailAddressAction.php +++ b/backend/app/Http/Actions/Users/ConfirmEmailAddressAction.php @@ -4,8 +4,8 @@ use HiEvents\DomainObjects\UserDomainObject; use HiEvents\Http\Actions\BaseAction; -use HiEvents\Services\Handlers\User\ConfirmEmailAddressHandler; -use HiEvents\Services\Handlers\User\DTO\ConfirmEmailChangeDTO; +use HiEvents\Services\Application\Handlers\User\ConfirmEmailAddressHandler; +use HiEvents\Services\Application\Handlers\User\DTO\ConfirmEmailChangeDTO; use HiEvents\Services\Infrastructure\Encryption\Exception\DecryptionFailedException; use HiEvents\Services\Infrastructure\Encryption\Exception\EncryptedPayloadExpiredException; use Illuminate\Http\JsonResponse; diff --git a/backend/app/Http/Actions/Users/ConfirmEmailChangeAction.php b/backend/app/Http/Actions/Users/ConfirmEmailChangeAction.php index 9bb8f39a..e443871b 100644 --- a/backend/app/Http/Actions/Users/ConfirmEmailChangeAction.php +++ b/backend/app/Http/Actions/Users/ConfirmEmailChangeAction.php @@ -6,8 +6,8 @@ use HiEvents\Exceptions\ResourceConflictException; use HiEvents\Http\Actions\BaseAction; use HiEvents\Resources\User\UserResource; -use HiEvents\Services\Handlers\User\ConfirmEmailChangeHandler; -use HiEvents\Services\Handlers\User\DTO\ConfirmEmailChangeDTO; +use HiEvents\Services\Application\Handlers\User\ConfirmEmailChangeHandler; +use HiEvents\Services\Application\Handlers\User\DTO\ConfirmEmailChangeDTO; use HiEvents\Services\Infrastructure\Encryption\Exception\DecryptionFailedException; use HiEvents\Services\Infrastructure\Encryption\Exception\EncryptedPayloadExpiredException; use Illuminate\Http\JsonResponse; diff --git a/backend/app/Http/Actions/Users/CreateUserAction.php b/backend/app/Http/Actions/Users/CreateUserAction.php index a1610010..072273fa 100644 --- a/backend/app/Http/Actions/Users/CreateUserAction.php +++ b/backend/app/Http/Actions/Users/CreateUserAction.php @@ -10,8 +10,8 @@ use HiEvents\Http\Request\User\CreateUserRequest; use HiEvents\Http\ResponseCodes; use HiEvents\Resources\User\UserResource; -use HiEvents\Services\Handlers\User\CreateUserHandler; -use HiEvents\Services\Handlers\User\DTO\CreateUserDTO; +use HiEvents\Services\Application\Handlers\User\CreateUserHandler; +use HiEvents\Services\Application\Handlers\User\DTO\CreateUserDTO; use Illuminate\Http\JsonResponse; use Illuminate\Validation\ValidationException; use Throwable; diff --git a/backend/app/Http/Actions/Users/ResendEmailConfirmationAction.php b/backend/app/Http/Actions/Users/ResendEmailConfirmationAction.php index 74cd5787..6035f7e1 100644 --- a/backend/app/Http/Actions/Users/ResendEmailConfirmationAction.php +++ b/backend/app/Http/Actions/Users/ResendEmailConfirmationAction.php @@ -3,7 +3,7 @@ namespace HiEvents\Http\Actions\Users; use HiEvents\Http\Actions\BaseAction; -use HiEvents\Services\Handlers\User\ResendEmailConfirmationHandler; +use HiEvents\Services\Application\Handlers\User\ResendEmailConfirmationHandler; use Illuminate\Http\Response; class ResendEmailConfirmationAction extends BaseAction diff --git a/backend/app/Http/Actions/Users/UpdateMeAction.php b/backend/app/Http/Actions/Users/UpdateMeAction.php index 69955805..4c1d442c 100644 --- a/backend/app/Http/Actions/Users/UpdateMeAction.php +++ b/backend/app/Http/Actions/Users/UpdateMeAction.php @@ -6,8 +6,8 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\User\UpdateMeRequest; use HiEvents\Resources\User\UserResource; -use HiEvents\Services\Handlers\User\DTO\UpdateMeDTO; -use HiEvents\Services\Handlers\User\UpdateMeHandler; +use HiEvents\Services\Application\Handlers\User\DTO\UpdateMeDTO; +use HiEvents\Services\Application\Handlers\User\UpdateMeHandler; use Illuminate\Http\JsonResponse; use Illuminate\Validation\ValidationException; diff --git a/backend/app/Http/Actions/Users/UpdateUserAction.php b/backend/app/Http/Actions/Users/UpdateUserAction.php index 6be10ac5..dae41bb6 100644 --- a/backend/app/Http/Actions/Users/UpdateUserAction.php +++ b/backend/app/Http/Actions/Users/UpdateUserAction.php @@ -8,8 +8,8 @@ use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\Request\User\UpdateUserRequest; use HiEvents\Resources\User\UserResource; -use HiEvents\Services\Handlers\User\DTO\UpdateUserDTO; -use HiEvents\Services\Handlers\User\UpdateUserHandler; +use HiEvents\Services\Application\Handlers\User\DTO\UpdateUserDTO; +use HiEvents\Services\Application\Handlers\User\UpdateUserHandler; use Illuminate\Http\JsonResponse; use Illuminate\Validation\ValidationException; use Throwable; diff --git a/backend/app/Http/Kernel.php b/backend/app/Http/Kernel.php index c8fbb3ba..c35b3b1a 100644 --- a/backend/app/Http/Kernel.php +++ b/backend/app/Http/Kernel.php @@ -90,5 +90,7 @@ class Kernel extends HttpKernel 'signed' => ValidateSignature::class, 'throttle' => ThrottleRequests::class, 'verified' => EnsureEmailIsVerified::class, + 'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class, + 'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class, ]; } diff --git a/backend/app/Http/Middleware/SetAccountContext.php b/backend/app/Http/Middleware/SetAccountContext.php index 042b58a1..5a35f884 100644 --- a/backend/app/Http/Middleware/SetAccountContext.php +++ b/backend/app/Http/Middleware/SetAccountContext.php @@ -5,16 +5,31 @@ use Closure; use HiEvents\Models\User; use Illuminate\Support\Facades\Auth; +use Laravel\Sanctum\TransientToken; class SetAccountContext { + public function handle($request, Closure $next) { if (Auth::check()) { - $accountId = Auth::payload()->get('account_id'); + if (Auth::user()->currentAccessToken()) { + if (Auth::user()->currentAccessToken() instanceof TransientToken) { + // assume logged in + $accountId = auth()->guard('api')->payload()->get('account_id'); + + if ($accountId) { + User::setCurrentAccountId($accountId); + } + } else { + User::setCurrentAccountId(Auth::user()->currentAccessToken()->account_id); + } + } else { + $accountId = Auth::payload()->get('account_id'); - if ($accountId) { - User::setCurrentAccountId($accountId); + if ($accountId) { + User::setCurrentAccountId($accountId); + } } } diff --git a/backend/app/Http/Request/Attendee/CreateAttendeeRequest.php b/backend/app/Http/Request/Attendee/CreateAttendeeRequest.php index 880c1881..3ff1502d 100644 --- a/backend/app/Http/Request/Attendee/CreateAttendeeRequest.php +++ b/backend/app/Http/Request/Attendee/CreateAttendeeRequest.php @@ -12,8 +12,8 @@ class CreateAttendeeRequest extends BaseRequest public function rules(): array { return [ - 'ticket_id' => ['int', 'required'], - 'ticket_price_id' => ['int', 'nullable', 'required'], + 'product_id' => ['int', 'required'], + 'product_price_id' => ['int', 'nullable', 'required'], 'email' => ['required', 'email'], 'first_name' => 'string|required', 'last_name' => 'string', diff --git a/backend/app/Http/Request/Attendee/EditAttendeeRequest.php b/backend/app/Http/Request/Attendee/EditAttendeeRequest.php index 10cf0ae4..780c42a8 100644 --- a/backend/app/Http/Request/Attendee/EditAttendeeRequest.php +++ b/backend/app/Http/Request/Attendee/EditAttendeeRequest.php @@ -13,8 +13,9 @@ public function rules(): array 'email' => RulesHelper::REQUIRED_EMAIL, 'first_name' => RulesHelper::REQUIRED_STRING, 'last_name' => RulesHelper::REQUIRED_STRING, - 'ticket_id' => RulesHelper::REQUIRED_NUMERIC, - 'ticket_price_id' => RulesHelper::REQUIRED_NUMERIC, + 'product_id' => RulesHelper::REQUIRED_NUMERIC, + 'product_price_id' => RulesHelper::REQUIRED_NUMERIC, + 'notes' => RulesHelper::OPTIONAL_TEXT_MEDIUM_LENGTH, ]; } @@ -25,10 +26,11 @@ public function messages(): array 'email.email' => __('Email must be a valid email address'), 'first_name.required' => __('First name is required'), 'last_name.required' => __('Last name is required'), - 'ticket_id.required' => __('Ticket is required'), - 'ticket_price_id.required' => __('Ticket price is required'), - 'ticket_id.numeric' => '', - 'ticket_price_id.numeric' => '', + 'product_id.required' => __('Product is required'), + 'product_price_id.required' => __('Product price is required'), + 'product_id.numeric' => '', + 'product_price_id.numeric' => '', + 'notes.max' => __('Notes must be less than 2000 characters'), ]; } } diff --git a/backend/app/Http/Request/CapacityAssigment/UpsertCapacityAssignmentRequest.php b/backend/app/Http/Request/CapacityAssigment/UpsertCapacityAssignmentRequest.php index 0873bfac..008000d4 100644 --- a/backend/app/Http/Request/CapacityAssigment/UpsertCapacityAssignmentRequest.php +++ b/backend/app/Http/Request/CapacityAssigment/UpsertCapacityAssignmentRequest.php @@ -15,14 +15,14 @@ public function rules(): array 'name' => RulesHelper::REQUIRED_STRING, 'capacity' => ['nullable', 'numeric', 'min:1'], 'status' => ['required', Rule::in(CapacityAssignmentStatus::valuesArray())], - 'ticket_ids' => ['required', 'array'], + 'product_ids' => ['required', 'array'], ]; } public function messages(): array { return [ - 'ticket_ids.required' => __('Please select at least one ticket.'), + 'product_ids.required' => __('Please select at least one product.'), ]; } } diff --git a/backend/app/Http/Request/CheckInList/UpsertCheckInListRequest.php b/backend/app/Http/Request/CheckInList/UpsertCheckInListRequest.php index 8405521d..06372e67 100644 --- a/backend/app/Http/Request/CheckInList/UpsertCheckInListRequest.php +++ b/backend/app/Http/Request/CheckInList/UpsertCheckInListRequest.php @@ -14,7 +14,7 @@ public function rules(): array 'description' => ['nullable', 'string', 'max:255'], 'expires_at' => ['nullable', 'date'], 'activates_at' => ['nullable', 'date'], - 'ticket_ids' => ['required', 'array', 'min:1'], + 'product_ids' => ['required', 'array', 'min:1'], ]; } @@ -32,7 +32,7 @@ public function withValidator($validator): void public function messages(): array { return [ - 'ticket_ids.required' => __('Please select at least one ticket.'), + 'product_ids.required' => __('Please select at least one product.'), 'expires_at.after' => __('The expiration date must be after the activation date.'), 'activates_at.before' => __('The activation date must be before the expiration date.'), ]; diff --git a/backend/app/Http/Request/Event/DuplicateEventRequest.php b/backend/app/Http/Request/Event/DuplicateEventRequest.php index 70e0420a..c172c118 100644 --- a/backend/app/Http/Request/Event/DuplicateEventRequest.php +++ b/backend/app/Http/Request/Event/DuplicateEventRequest.php @@ -14,7 +14,7 @@ public function rules(): array $eventValidations = $this->minimalRules(); $duplicateValidations = [ - 'duplicate_tickets' => ['boolean', 'required'], + 'duplicate_products' => ['boolean', 'required'], 'duplicate_questions' => ['boolean', 'required'], 'duplicate_settings' => ['boolean', 'required'], 'duplicate_promo_codes' => ['boolean', 'required'], diff --git a/backend/app/Http/Request/EventSettings/UpdateEventSettingsRequest.php b/backend/app/Http/Request/EventSettings/UpdateEventSettingsRequest.php index 4b1ad922..5942b7c7 100644 --- a/backend/app/Http/Request/EventSettings/UpdateEventSettingsRequest.php +++ b/backend/app/Http/Request/EventSettings/UpdateEventSettingsRequest.php @@ -69,8 +69,8 @@ public function messages(): array 'homepage_text_color' => $colorMessage, 'homepage_button_color' => $colorMessage, 'homepage_link_color' => $colorMessage, - 'homepage_ticket_widget_background_color' => $colorMessage, - 'homepage_ticket_widget_text_color' => $colorMessage, + 'homepage_product_widget_background_color' => $colorMessage, + 'homepage_product_widget_text_color' => $colorMessage, 'location_details.address_line_1.required_with' => __('The address line 1 field is required'), 'location_details.city.required_with' => __('The city field is required'), 'location_details.zip_or_postal_code.required_with' => __('The zip or postal code field is required'), diff --git a/backend/app/Http/Request/Message/SendMessageRequest.php b/backend/app/Http/Request/Message/SendMessageRequest.php index 35f69c7b..5eb7851d 100644 --- a/backend/app/Http/Request/Message/SendMessageRequest.php +++ b/backend/app/Http/Request/Message/SendMessageRequest.php @@ -17,9 +17,9 @@ public function rules(): array 'is_test' => 'boolean', 'attendee_ids' => 'max:50,array|required_if:message_type,' . MessageTypeEnum::ATTENDEE->name, 'attendee_ids.*' => 'integer', - 'ticket_ids' => ['array', 'required_if:message_type,' . MessageTypeEnum::TICKET->name], + 'product_ids' => ['array', 'required_if:message_type,' . MessageTypeEnum::PRODUCT->name], 'order_id' => 'integer|required_if:message_type,' . MessageTypeEnum::ORDER->name, - 'ticket_ids.*' => 'integer', + 'product_ids.*' => 'integer', ]; } @@ -28,7 +28,7 @@ public function messages(): array return [ 'subject.max' => 'The subject must be less than 100 characters.', 'attendee_ids.max' => 'You can only send a message to a maximum of 50 individual attendees at a time. ' . - 'To message more attendees, you can send to attendees with a specific ticket, or to all event attendees.' + 'To message more attendees, you can send to attendees with a specific product, or to all event attendees.' ]; } } diff --git a/backend/app/Http/Request/Product/SortProductsRequest.php b/backend/app/Http/Request/Product/SortProductsRequest.php new file mode 100644 index 00000000..5c78b486 --- /dev/null +++ b/backend/app/Http/Request/Product/SortProductsRequest.php @@ -0,0 +1,19 @@ + 'array|required', + 'sorted_categories.*.product_category_id' => 'integer|required', + 'sorted_categories.*.sorted_products' => 'array', + 'sorted_categories.*.sorted_products.*.id' => 'integer|required', + 'sorted_categories.*.sorted_products.*.order' => 'integer', + ]; + } +} diff --git a/backend/app/Http/Request/Ticket/UpsertTicketRequest.php b/backend/app/Http/Request/Product/UpsertProductRequest.php similarity index 76% rename from backend/app/Http/Request/Ticket/UpsertTicketRequest.php rename to backend/app/Http/Request/Product/UpsertProductRequest.php index 4eff2df3..eb8f91b5 100644 --- a/backend/app/Http/Request/Ticket/UpsertTicketRequest.php +++ b/backend/app/Http/Request/Product/UpsertProductRequest.php @@ -2,14 +2,15 @@ declare(strict_types=1); -namespace HiEvents\Http\Request\Ticket; +namespace HiEvents\Http\Request\Product; -use HiEvents\DomainObjects\Enums\TicketType; +use HiEvents\DomainObjects\Enums\ProductPriceType; +use HiEvents\DomainObjects\Enums\ProductType; use HiEvents\Http\Request\BaseRequest; use HiEvents\Validators\Rules\RulesHelper; use Illuminate\Validation\Rule; -class UpsertTicketRequest extends BaseRequest +class UpsertProductRequest extends BaseRequest { public function rules(): array { @@ -22,7 +23,7 @@ public function rules(): array 'max_per_order' => 'integer|nullable', 'prices' => ['required', 'array'], 'prices.*.price' => [...RulesHelper::MONEY, 'required'], - 'prices.*.label' => ['nullable', ...RulesHelper::STRING, 'required_if:type,' . TicketType::TIERED->name], + 'prices.*.label' => ['nullable', ...RulesHelper::STRING, 'required_if:type,' . ProductPriceType::TIERED->name], 'prices.*.sale_start_date' => ['date', 'nullable', 'after:sale_start_date'], 'prices.*.sale_end_date' => 'date|nullable|after:prices.*.sale_start_date', 'prices.*.initial_quantity_available' => ['integer', 'nullable', 'min:0'], @@ -36,8 +37,10 @@ public function rules(): array 'start_collapsed' => 'boolean', 'show_quantity_remaining' => 'boolean', 'is_hidden_without_promo_code' => 'boolean', - 'type' => ['required', Rule::in(TicketType::valuesArray())], + 'type' => ['required', Rule::in(ProductPriceType::valuesArray())], + 'product_type' => ['required', Rule::in(ProductType::valuesArray())], 'tax_and_fee_ids' => 'array', + 'product_category_id' => ['required', 'integer'], ]; } @@ -47,7 +50,8 @@ public function messages(): array 'sale_end_date.after' => __('The sale end date must be after the sale start date.'), 'prices.*.sale_end_date.after' => __('The sale end date must be after the sale start date.'), 'prices.*.sale_end_date.date' => __('The sale end date must be a valid date.'), - 'prices.*.sale_start_date.after' => __('The sale start date must be after the ticket sale start date.'), + 'prices.*.sale_start_date.after' => __('The sale start date must be after the product sale start date.'), + 'product_category_id.required' => __('You must select a product category.'), ]; } } diff --git a/backend/app/Http/Request/ProductCategory/UpsertProductCategoryRequest.php b/backend/app/Http/Request/ProductCategory/UpsertProductCategoryRequest.php new file mode 100644 index 00000000..87a7ccdb --- /dev/null +++ b/backend/app/Http/Request/ProductCategory/UpsertProductCategoryRequest.php @@ -0,0 +1,18 @@ + ['string', 'required', 'max:50'], + 'description' => ['string', 'max:255', 'nullable'], + 'is_hidden' => ['boolean', 'required'], + 'no_products_message' => ['string', 'max:255', 'nullable'], + ]; + } +} diff --git a/backend/app/Http/Request/PromoCode/CreateUpdatePromoCodeRequest.php b/backend/app/Http/Request/PromoCode/CreateUpdatePromoCodeRequest.php index 328fd0cb..5e381f61 100644 --- a/backend/app/Http/Request/PromoCode/CreateUpdatePromoCodeRequest.php +++ b/backend/app/Http/Request/PromoCode/CreateUpdatePromoCodeRequest.php @@ -12,7 +12,7 @@ public function rules(): array { return [ 'code' => 'min:2|string|required|max:50', - 'applicable_ticket_ids' => 'array', + 'applicable_product_ids' => 'array', 'discount' => [ 'required_if:discount_type,PERCENTAGE,FIXED', 'numeric', diff --git a/backend/app/Http/Request/Questions/UpsertQuestionRequest.php b/backend/app/Http/Request/Questions/UpsertQuestionRequest.php index e696d15e..a383a003 100644 --- a/backend/app/Http/Request/Questions/UpsertQuestionRequest.php +++ b/backend/app/Http/Request/Questions/UpsertQuestionRequest.php @@ -15,9 +15,9 @@ public function rules(): array 'title' => ['string', 'required'], 'description' => ['string', 'nullable', 'max:10000'], 'type' => ['required', Rule::in(QuestionTypeEnum::valuesArray())], - 'ticket_ids' => ['array', 'required_if:belongs_to,TICKET'], + 'product_ids' => ['array', 'required_if:belongs_to,PRODUCT'], 'belongs_to' => [ - ['required', Rule::in([QuestionBelongsTo::TICKET->name, QuestionBelongsTo::ORDER->name])], + ['required', Rule::in([QuestionBelongsTo::PRODUCT->name, QuestionBelongsTo::ORDER->name])], ], 'options' => 'max:2000|required_if:type,CHECKBOX,RADIO', 'required' => 'required|boolean', @@ -28,7 +28,7 @@ public function rules(): array public function messages(): array { return [ - 'ticket_ids.required_if' => __('Please select at least one ticket.'), + 'product_ids.required_if' => __('Please select at least one product.'), ]; } } diff --git a/backend/app/Http/Request/Report/GetReportRequest.php b/backend/app/Http/Request/Report/GetReportRequest.php new file mode 100644 index 00000000..458a9861 --- /dev/null +++ b/backend/app/Http/Request/Report/GetReportRequest.php @@ -0,0 +1,16 @@ + 'date|before:end_date|required_with:end_date|nullable', + 'end_date' => 'date|after:start_date|required_with:start_date|nullable', + ]; + } +} diff --git a/backend/app/Http/Request/Ticket/SortTicketsRequest.php b/backend/app/Http/Request/Ticket/SortTicketsRequest.php deleted file mode 100644 index 5ca72804..00000000 --- a/backend/app/Http/Request/Ticket/SortTicketsRequest.php +++ /dev/null @@ -1,16 +0,0 @@ - 'integer|required', - '*.order' => 'integer|required', - ]; - } -} diff --git a/backend/app/Jobs/Event/SendMessagesJob.php b/backend/app/Jobs/Event/SendMessagesJob.php index eaa087dc..142f322f 100644 --- a/backend/app/Jobs/Event/SendMessagesJob.php +++ b/backend/app/Jobs/Event/SendMessagesJob.php @@ -3,8 +3,8 @@ namespace HiEvents\Jobs\Event; use HiEvents\Exceptions\UnableToSendMessageException; +use HiEvents\Services\Application\Handlers\Message\DTO\SendMessageDTO; use HiEvents\Services\Domain\Mail\SendEventEmailMessagesService; -use HiEvents\Services\Handlers\Message\DTO\SendMessageDTO; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; diff --git a/backend/app/Jobs/Order/SendOrderDetailsEmailJob.php b/backend/app/Jobs/Order/SendOrderDetailsEmailJob.php index c6fb63f9..4ca2fcf1 100644 --- a/backend/app/Jobs/Order/SendOrderDetailsEmailJob.php +++ b/backend/app/Jobs/Order/SendOrderDetailsEmailJob.php @@ -22,6 +22,6 @@ public function __construct(private readonly OrderDomainObject $order) public function handle(SendOrderDetailsService $service): void { - $service->sendOrderSummaryAndTicketEmails($this->order); + $service->sendOrderSummaryAndProductEmails($this->order); } } diff --git a/backend/app/Mail/Event/EventMessage.php b/backend/app/Mail/Event/EventMessage.php index 11a3a892..f7807374 100644 --- a/backend/app/Mail/Event/EventMessage.php +++ b/backend/app/Mail/Event/EventMessage.php @@ -5,7 +5,7 @@ use HiEvents\DomainObjects\EventDomainObject; use HiEvents\DomainObjects\EventSettingDomainObject; use HiEvents\Mail\BaseMail; -use HiEvents\Services\Handlers\Message\DTO\SendMessageDTO; +use HiEvents\Services\Application\Handlers\Message\DTO\SendMessageDTO; use Illuminate\Mail\Mailables\Content; use Illuminate\Mail\Mailables\Envelope; diff --git a/backend/app/Models/Attendee.php b/backend/app/Models/Attendee.php index 9164ce23..476a6d16 100644 --- a/backend/app/Models/Attendee.php +++ b/backend/app/Models/Attendee.php @@ -30,9 +30,9 @@ public function order(): BelongsTo return $this->belongsTo(Order::class); } - public function ticket(): BelongsTo + public function product(): BelongsTo { - return $this->belongsTo(Ticket::class); + return $this->belongsTo(Product::class); } public function check_in(): HasOne diff --git a/backend/app/Models/AttendeeCheckIn.php b/backend/app/Models/AttendeeCheckIn.php index be8f6f68..50eff851 100644 --- a/backend/app/Models/AttendeeCheckIn.php +++ b/backend/app/Models/AttendeeCheckIn.php @@ -16,10 +16,10 @@ protected function getFillableFields(): array return []; } - public function tickets(): BelongsTo + public function products(): BelongsTo { return $this->belongsTo( - related: Ticket::class, + related: Product::class, ); } diff --git a/backend/app/Models/CapacityAssignment.php b/backend/app/Models/CapacityAssignment.php index 7359f945..87518f13 100644 --- a/backend/app/Models/CapacityAssignment.php +++ b/backend/app/Models/CapacityAssignment.php @@ -22,11 +22,11 @@ public function event(): BelongsTo return $this->belongsTo(Event::class); } - public function tickets(): BelongsToMany + public function products(): BelongsToMany { return $this->belongsToMany( - related: Ticket::class, - table: 'ticket_capacity_assignments', + related: Product::class, + table: 'product_capacity_assignments', ); } } diff --git a/backend/app/Models/CheckInList.php b/backend/app/Models/CheckInList.php index d61c62b9..0e918372 100644 --- a/backend/app/Models/CheckInList.php +++ b/backend/app/Models/CheckInList.php @@ -17,11 +17,11 @@ protected function getFillableFields(): array return []; } - public function tickets(): BelongsToMany + public function products(): BelongsToMany { return $this->belongsToMany( - related: Ticket::class, - table: 'ticket_check_in_lists', + related: Product::class, + table: 'product_check_in_lists', ); } diff --git a/backend/app/Models/Event.php b/backend/app/Models/Event.php index d7be8789..d87972ee 100644 --- a/backend/app/Models/Event.php +++ b/backend/app/Models/Event.php @@ -19,9 +19,14 @@ public function organizer(): BelongsTo return $this->belongsTo(Organizer::class); } - public function tickets(): HasMany + public function products(): HasMany { - return $this->hasMany(Ticket::class)->orderBy('order'); + return $this->hasMany(Product::class)->orderBy('order'); + } + + public function product_categories(): HasMany + { + return $this->hasMany(ProductCategory::class)->orderBy('order'); } public function attendees(): HasMany diff --git a/backend/app/Models/Message.php b/backend/app/Models/Message.php index 7e08f7ae..1cac8fae 100644 --- a/backend/app/Models/Message.php +++ b/backend/app/Models/Message.php @@ -15,7 +15,7 @@ protected function getCastMap(): array { return [ 'attendee_ids' => 'array', - 'ticket_ids' => 'array', + 'product_ids' => 'array', 'send_data' => 'array', ]; } diff --git a/backend/app/Models/OrderItem.php b/backend/app/Models/OrderItem.php index b1b87bd9..cd579670 100644 --- a/backend/app/Models/OrderItem.php +++ b/backend/app/Models/OrderItem.php @@ -36,13 +36,13 @@ protected function getFillableFields(): array return []; } - public function ticket_price(): HasOne + public function product_price(): HasOne { - return $this->hasOne(TicketPrice::class); + return $this->hasOne(ProductPrice::class); } - public function ticket(): BelongsTo + public function product(): BelongsTo { - return $this->belongsTo(Ticket::class); + return $this->belongsTo(Product::class); } } diff --git a/backend/app/Models/PersonalAccessToken.php b/backend/app/Models/PersonalAccessToken.php new file mode 100644 index 00000000..f97a08fd --- /dev/null +++ b/backend/app/Models/PersonalAccessToken.php @@ -0,0 +1,21 @@ +mergeFillable(['account_id']); + } +} \ No newline at end of file diff --git a/backend/app/Models/Product.php b/backend/app/Models/Product.php new file mode 100644 index 00000000..a8208578 --- /dev/null +++ b/backend/app/Models/Product.php @@ -0,0 +1,56 @@ + 'float', + ProductDomainObjectAbstract::SALES_TAX_VOLUME => 'float', + ]; + } + + protected function getFillableFields(): array + { + return []; + } + + public function questions(): BelongsToMany + { + return $this->belongsToMany(Question::class, 'product_questions'); + } + + public function product_prices(): HasMany + { + return $this->hasMany(ProductPrice::class)->orderBy('order'); + } + + public function tax_and_fees(): BelongsToMany + { + return $this->belongsToMany(TaxAndFee::class, 'product_taxes_and_fees'); + } + + public function capacity_assignments(): BelongsToMany + { + return $this->belongsToMany(CapacityAssignment::class, 'product_capacity_assignments'); + } + + public function check_in_lists(): BelongsToMany + { + return $this->belongsToMany(CheckInList::class, 'product_check_in_lists'); + } + + public function product_category(): BelongsTo + { + return $this->belongsTo(ProductCategory::class); + } +} diff --git a/backend/app/Models/ProductCategory.php b/backend/app/Models/ProductCategory.php new file mode 100644 index 00000000..b96826a2 --- /dev/null +++ b/backend/app/Models/ProductCategory.php @@ -0,0 +1,35 @@ +hasMany(Product::class); + } +} diff --git a/backend/app/Models/TicketPrice.php b/backend/app/Models/ProductPrice.php similarity index 70% rename from backend/app/Models/TicketPrice.php rename to backend/app/Models/ProductPrice.php index 1ac6e299..23ab15a3 100644 --- a/backend/app/Models/TicketPrice.php +++ b/backend/app/Models/ProductPrice.php @@ -4,7 +4,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; -class TicketPrice extends BaseModel +class ProductPrice extends BaseModel { protected function getCastMap(): array { @@ -18,8 +18,8 @@ protected function getFillableFields(): array return []; } - public function ticket(): BelongsTo + public function product(): BelongsTo { - return $this->belongsTo(Ticket::class); + return $this->belongsTo(Product::class); } } diff --git a/backend/app/Models/TicketQuestion.php b/backend/app/Models/ProductQuestion.php similarity index 55% rename from backend/app/Models/TicketQuestion.php rename to backend/app/Models/ProductQuestion.php index 4c85d75c..fee35fc5 100644 --- a/backend/app/Models/TicketQuestion.php +++ b/backend/app/Models/ProductQuestion.php @@ -2,9 +2,9 @@ namespace HiEvents\Models; -use HiEvents\DomainObjects\Generated\TicketQuestionDomainObjectAbstract; +use HiEvents\DomainObjects\Generated\ProductQuestionDomainObjectAbstract; -class TicketQuestion extends BaseModel +class ProductQuestion extends BaseModel { protected function getTimestampsEnabled(): bool { @@ -19,8 +19,8 @@ protected function getCastMap(): array protected function getFillableFields(): array { return [ - TicketQuestionDomainObjectAbstract::QUESTION_ID, - TicketQuestionDomainObjectAbstract::TICKET_ID, + ProductQuestionDomainObjectAbstract::QUESTION_ID, + ProductQuestionDomainObjectAbstract::PRODUCT_ID, ]; } } diff --git a/backend/app/Models/PromoCode.php b/backend/app/Models/PromoCode.php index d6d7545a..1e0ad4f0 100644 --- a/backend/app/Models/PromoCode.php +++ b/backend/app/Models/PromoCode.php @@ -11,7 +11,7 @@ protected function getCastMap(): array return [ PromoCodeDomainObjectAbstract::DISCOUNT => 'float', PromoCodeDomainObjectAbstract::EXPIRY_DATE => 'datetime', - PromoCodeDomainObjectAbstract::APPLICABLE_TICKET_IDS => 'array', + PromoCodeDomainObjectAbstract::APPLICABLE_PRODUCT_IDS => 'array', ]; } @@ -21,7 +21,7 @@ protected function getFillableFields(): array PromoCodeDomainObjectAbstract::CODE, PromoCodeDomainObjectAbstract::DISCOUNT, PromoCodeDomainObjectAbstract::DISCOUNT_TYPE, - PromoCodeDomainObjectAbstract::APPLICABLE_TICKET_IDS, + PromoCodeDomainObjectAbstract::APPLICABLE_PRODUCT_IDS, PromoCodeDomainObjectAbstract::EXPIRY_DATE, PromoCodeDomainObjectAbstract::EVENT_ID, PromoCodeDomainObjectAbstract::MAX_ALLOWED_USAGES, diff --git a/backend/app/Models/Question.php b/backend/app/Models/Question.php index 1fa2744f..f540466f 100644 --- a/backend/app/Models/Question.php +++ b/backend/app/Models/Question.php @@ -19,10 +19,10 @@ protected function getFillableFields(): array return []; } - public function tickets(): BelongsToMany + public function products(): BelongsToMany { return $this - ->belongsToMany(Ticket::class, 'ticket_questions') - ->whereNull('ticket_questions.deleted_at'); + ->belongsToMany(Product::class, 'product_questions') + ->whereNull('product_questions.deleted_at'); } } diff --git a/backend/app/Models/QuestionAnswer.php b/backend/app/Models/QuestionAnswer.php index 3564d22c..c9c4de13 100644 --- a/backend/app/Models/QuestionAnswer.php +++ b/backend/app/Models/QuestionAnswer.php @@ -17,7 +17,7 @@ protected function getFillableFields(): array { return [ QuestionAnswerDomainObjectAbstract::QUESTION_ID, - QuestionAnswerDomainObjectAbstract::TICKET_ID, + QuestionAnswerDomainObjectAbstract::PRODUCT_ID, QuestionAnswerDomainObjectAbstract::ORDER_ID, QuestionAnswerDomainObjectAbstract::ATTENDEE_ID, QuestionAnswerDomainObjectAbstract::ANSWER, diff --git a/backend/app/Models/TaxAndFee.php b/backend/app/Models/TaxAndFee.php index 32a656b9..1d36edbe 100644 --- a/backend/app/Models/TaxAndFee.php +++ b/backend/app/Models/TaxAndFee.php @@ -8,9 +8,9 @@ class TaxAndFee extends BaseModel { protected $table = 'taxes_and_fees'; - public function tickets(): BelongsToMany + public function products(): BelongsToMany { - return $this->belongsToMany(Ticket::class, 'ticket_taxes_and_fees'); + return $this->belongsToMany(Product::class, 'product_taxes_and_fees'); } protected function getCastMap(): array diff --git a/backend/app/Models/Ticket.php b/backend/app/Models/Ticket.php deleted file mode 100644 index dea99092..00000000 --- a/backend/app/Models/Ticket.php +++ /dev/null @@ -1,50 +0,0 @@ - 'float', - TicketDomainObjectAbstract::SALES_TAX_VOLUME => 'float', - ]; - } - - protected function getFillableFields(): array - { - return []; - } - - public function questions(): BelongsToMany - { - return $this->belongsToMany(Question::class, 'ticket_questions'); - } - - public function ticket_prices(): HasMany - { - return $this->hasMany(TicketPrice::class)->orderBy('order'); - } - - public function tax_and_fees(): BelongsToMany - { - return $this->belongsToMany(TaxAndFee::class, 'ticket_taxes_and_fees'); - } - - public function capacity_assignments(): BelongsToMany - { - return $this->belongsToMany(CapacityAssignment::class, 'ticket_capacity_assignments'); - } - - public function check_in_lists(): BelongsToMany - { - return $this->belongsToMany(CheckInList::class, 'ticket_check_in_lists'); - } -} diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php index 8a9a44df..e98fdeb8 100644 --- a/backend/app/Models/User.php +++ b/backend/app/Models/User.php @@ -11,12 +11,17 @@ use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOneThrough; use Illuminate\Foundation\Auth\Access\Authorizable; use Illuminate\Notifications\Notifiable; +use Illuminate\Support\Str; +use Laravel\Sanctum\HasApiTokens; +use Laravel\Sanctum\NewAccessToken; use PHPOpenSourceSaver\JWTAuth\Contracts\JWTSubject; use RuntimeException; +use DateTimeInterface; class User extends BaseModel implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, JWTSubject { @@ -25,6 +30,8 @@ class User extends BaseModel implements AuthenticatableContract, AuthorizableCon use Authorizable; use CanResetPassword; use MustVerifyEmail; + use HasApiTokens; + use HasFactory; /** @var array */ protected $guarded = []; @@ -92,4 +99,19 @@ public function currentAccountUser(): HasOne return $this->hasOne(AccountUser::class) ->where('account_id', static::getCurrentAccountId()); } + + public function createToken(string $name, array $abilities = ['*'], ?DateTimeInterface $expiresAt = null) + { + $plainTextToken = $this->generateTokenString(); + + $token = $this->tokens()->create([ + 'name' => $name, + 'token' => hash('sha256', $plainTextToken), + 'abilities' => $abilities, + 'expires_at' => $expiresAt, + 'account_id' => $this->getCurrentAccountId(), + ]); + + return new NewAccessToken($token, $token->getKey().'|'.$plainTextToken); + } } diff --git a/backend/app/Providers/AppServiceProvider.php b/backend/app/Providers/AppServiceProvider.php index 3499b364..03beba44 100644 --- a/backend/app/Providers/AppServiceProvider.php +++ b/backend/app/Providers/AppServiceProvider.php @@ -9,11 +9,14 @@ use HiEvents\DomainObjects\OrganizerDomainObject; use HiEvents\Models\Event; use HiEvents\Models\Organizer; +use HiEvents\Models\PersonalAccessToken; +use HiEvents\Models\User; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\File; use Illuminate\Support\ServiceProvider; +use Laravel\Sanctum\Sanctum; use Stripe\StripeClient; class AppServiceProvider extends ServiceProvider @@ -40,11 +43,14 @@ static function ($query) { ); } + Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class); + Model::preventLazyLoading(!app()->isProduction()); Relation::enforceMorphMap([ EventDomainObject::class => Event::class, OrganizerDomainObject::class => Organizer::class, + 'user' => User::class, ]); } diff --git a/backend/app/Providers/RepositoryServiceProvider.php b/backend/app/Providers/RepositoryServiceProvider.php index 2ae3adc9..046f41a3 100644 --- a/backend/app/Providers/RepositoryServiceProvider.php +++ b/backend/app/Providers/RepositoryServiceProvider.php @@ -21,14 +21,15 @@ use HiEvents\Repository\Eloquent\OrganizerRepository; use HiEvents\Repository\Eloquent\PasswordResetRepository; use HiEvents\Repository\Eloquent\PasswordResetTokenRepository; +use HiEvents\Repository\Eloquent\ProductCategoryRepository; use HiEvents\Repository\Eloquent\PromoCodeRepository; use HiEvents\Repository\Eloquent\QuestionAnswerRepository; use HiEvents\Repository\Eloquent\QuestionRepository; use HiEvents\Repository\Eloquent\StripeCustomerRepository; use HiEvents\Repository\Eloquent\StripePaymentsRepository; use HiEvents\Repository\Eloquent\TaxAndFeeRepository; -use HiEvents\Repository\Eloquent\TicketPriceRepository; -use HiEvents\Repository\Eloquent\TicketRepository; +use HiEvents\Repository\Eloquent\ProductPriceRepository; +use HiEvents\Repository\Eloquent\ProductRepository; use HiEvents\Repository\Eloquent\UserRepository; use HiEvents\Repository\Interfaces\AccountRepositoryInterface; use HiEvents\Repository\Interfaces\AccountUserRepositoryInterface; @@ -47,14 +48,15 @@ use HiEvents\Repository\Interfaces\OrganizerRepositoryInterface; use HiEvents\Repository\Interfaces\PasswordResetRepositoryInterface; use HiEvents\Repository\Interfaces\PasswordResetTokenRepositoryInterface; +use HiEvents\Repository\Interfaces\ProductCategoryRepositoryInterface; use HiEvents\Repository\Interfaces\PromoCodeRepositoryInterface; use HiEvents\Repository\Interfaces\QuestionAnswerRepositoryInterface; use HiEvents\Repository\Interfaces\QuestionRepositoryInterface; use HiEvents\Repository\Interfaces\StripeCustomerRepositoryInterface; use HiEvents\Repository\Interfaces\StripePaymentsRepositoryInterface; use HiEvents\Repository\Interfaces\TaxAndFeeRepositoryInterface; -use HiEvents\Repository\Interfaces\TicketPriceRepositoryInterface; -use HiEvents\Repository\Interfaces\TicketRepositoryInterface; +use HiEvents\Repository\Interfaces\ProductPriceRepositoryInterface; +use HiEvents\Repository\Interfaces\ProductRepositoryInterface; use HiEvents\Repository\Interfaces\UserRepositoryInterface; use Illuminate\Support\ServiceProvider; @@ -67,7 +69,7 @@ class RepositoryServiceProvider extends ServiceProvider UserRepositoryInterface::class => UserRepository::class, AccountRepositoryInterface::class => AccountRepository::class, EventRepositoryInterface::class => EventRepository::class, - TicketRepositoryInterface::class => TicketRepository::class, + ProductRepositoryInterface::class => ProductRepository::class, OrderRepositoryInterface::class => OrderRepository::class, AttendeeRepositoryInterface::class => AttendeeRepository::class, OrderItemRepositoryInterface::class => OrderItemRepository::class, @@ -80,7 +82,7 @@ class RepositoryServiceProvider extends ServiceProvider PasswordResetRepositoryInterface::class => PasswordResetRepository::class, TaxAndFeeRepositoryInterface::class => TaxAndFeeRepository::class, ImageRepositoryInterface::class => ImageRepository::class, - TicketPriceRepositoryInterface::class => TicketPriceRepository::class, + ProductPriceRepositoryInterface::class => ProductPriceRepository::class, EventStatisticRepositoryInterface::class => EventStatisticRepository::class, EventDailyStatisticRepositoryInterface::class => EventDailyStatisticRepository::class, EventSettingsRepositoryInterface::class => EventSettingsRepository::class, @@ -90,6 +92,7 @@ class RepositoryServiceProvider extends ServiceProvider StripeCustomerRepositoryInterface::class => StripeCustomerRepository::class, CheckInListRepositoryInterface::class => CheckInListRepository::class, AttendeeCheckInRepositoryInterface::class => AttendeeCheckInRepository::class, + ProductCategoryRepositoryInterface::class => ProductCategoryRepository::class, ]; public function register(): void diff --git a/backend/app/Repository/Eloquent/AttendeeRepository.php b/backend/app/Repository/Eloquent/AttendeeRepository.php index a60f3439..e0e16e38 100644 --- a/backend/app/Repository/Eloquent/AttendeeRepository.php +++ b/backend/app/Repository/Eloquent/AttendeeRepository.php @@ -108,8 +108,8 @@ public function getAttendeesByCheckInShortId(string $shortId, QueryParamsDTO $pa $this->model = $this->model->select('attendees.*') ->join('orders', 'orders.id', '=', 'attendees.order_id') - ->join('ticket_check_in_lists', 'ticket_check_in_lists.ticket_id', '=', 'attendees.ticket_id') - ->join('check_in_lists', 'check_in_lists.id', '=', 'ticket_check_in_lists.check_in_list_id') + ->join('product_check_in_lists', 'product_check_in_lists.product_id', '=', 'attendees.product_id') + ->join('check_in_lists', 'check_in_lists.id', '=', 'product_check_in_lists.check_in_list_id') ->where('check_in_lists.short_id', $shortId) ->where('attendees.status', AttendeeStatus::ACTIVE->name) ->whereIn('orders.status', [OrderStatus::COMPLETED->name]); diff --git a/backend/app/Repository/Eloquent/BaseRepository.php b/backend/app/Repository/Eloquent/BaseRepository.php index c781a44b..1be0bc05 100644 --- a/backend/app/Repository/Eloquent/BaseRepository.php +++ b/backend/app/Repository/Eloquent/BaseRepository.php @@ -128,10 +128,25 @@ public function findFirst(int $id, array $columns = self::DEFAULT_COLUMNS): ?Dom return $this->handleSingleResult($this->model->findOrFail($id, $columns)); } - public function findWhere(array $where, array $columns = self::DEFAULT_COLUMNS): Collection + public function findWhere( + array $where, + array $columns = self::DEFAULT_COLUMNS, + array $orderAndDirections = [], + ): Collection { $this->applyConditions($where); + + if ($orderAndDirections) { + foreach ($orderAndDirections as $orderAndDirection) { + $this->model = $this->model->orderBy( + $orderAndDirection->getOrder(), + $orderAndDirection->getDirection() + ); + } + } + $model = $this->model->get($columns); + $this->resetModel(); return $this->handleResults($model); diff --git a/backend/app/Repository/Eloquent/CheckInListRepository.php b/backend/app/Repository/Eloquent/CheckInListRepository.php index e539b130..c3be46d0 100644 --- a/backend/app/Repository/Eloquent/CheckInListRepository.php +++ b/backend/app/Repository/Eloquent/CheckInListRepository.php @@ -38,7 +38,7 @@ public function getCheckedInAttendeeCountById(int $checkInListId): CheckedInAtte valid_attendees AS ( SELECT a.id, tcil.check_in_list_id FROM attendees a - JOIN ticket_check_in_lists tcil ON a.ticket_id = tcil.ticket_id + JOIN product_check_in_lists tcil ON a.product_id = tcil.product_id WHERE a.deleted_at IS NULL AND tcil.deleted_at IS NULL AND a.status = 'ACTIVE' @@ -79,7 +79,7 @@ public function getCheckedInAttendeeCountByIds(array $checkInListIds): Collectio valid_attendees AS ( SELECT a.id, tcil.check_in_list_id FROM attendees a - JOIN ticket_check_in_lists tcil ON a.ticket_id = tcil.ticket_id + JOIN product_check_in_lists tcil ON a.product_id = tcil.product_id WHERE a.deleted_at IS NULL AND tcil.deleted_at IS NULL AND a.status = '$attendeeActiveStatus' diff --git a/backend/app/Repository/Eloquent/ProductCategoryRepository.php b/backend/app/Repository/Eloquent/ProductCategoryRepository.php new file mode 100644 index 00000000..6a1546c4 --- /dev/null +++ b/backend/app/Repository/Eloquent/ProductCategoryRepository.php @@ -0,0 +1,50 @@ +model + ->where('event_id', $eventId) + ->with(['products']); + + // Apply filters from QueryParamsDTO, if needed + if (!empty($queryParamsDTO->filter_fields)) { + foreach ($queryParamsDTO->filter_fields as $filter) { + $query->where($filter->field, $filter->operator ?? '=', $filter->value); + } + } + + // Apply sorting from QueryParamsDTO + if (!empty($queryParamsDTO->sort_by)) { + $query->orderBy($queryParamsDTO->sort_by, $queryParamsDTO->sort_direction ?? 'asc'); + } + + return $query->get(); + } + + public function getNextOrder(int $eventId) + { + return $this->model + ->where('event_id', $eventId) + ->max('order') + 1; + } +} diff --git a/backend/app/Repository/Eloquent/ProductPriceRepository.php b/backend/app/Repository/Eloquent/ProductPriceRepository.php new file mode 100644 index 00000000..7114d6f3 --- /dev/null +++ b/backend/app/Repository/Eloquent/ProductPriceRepository.php @@ -0,0 +1,20 @@ +query)) { + $where[] = static function (Builder $builder) use ($params) { + $builder + ->where(ProductDomainObjectAbstract::TITLE, 'ilike', '%' . $params->query . '%'); + }; + } + + $this->model = $this->model->orderBy( + $params->sort_by ?? ProductDomainObject::getDefaultSort(), + $params->sort_direction ?? ProductDomainObject::getDefaultSortDirection(), + ); + + return $this->paginateWhere( + where: $where, + limit: $params->per_page, + page: $params->page, + ); + } + + /** + * @param int $productId + * @param int $productPriceId + * @return int + */ + public function getQuantityRemainingForProductPrice(int $productId, int $productPriceId): int + { + $query = <<db->selectOne($query, [ + 'productPriceId' => $productPriceId, + 'productId' => $productId + ]); + + if ($result === null) { + throw new RuntimeException('Product price not found'); + } + + if ($result->unlimited_products_available) { + return Constants::INFINITE; + } + + return (int)$result->quantity_remaining; + } + + public function getTaxesByProductId(int $productId): Collection + { + $query = <<db->select($query, [ + 'productId' => $productId + ]); + + return $this->handleResults($taxAndFees, TaxAndFeesDomainObject::class); + } + + public function getProductsByTaxId(int $taxId): Collection + { + $query = <<model->select($query, [ + 'taxAndFeeId' => $taxId + ]); + + return $this->handleResults($products, ProductDomainObject::class); + } + + public function getCapacityAssignmentsByProductId(int $productId): Collection + { + $capacityAssignments = CapacityAssignment::whereHas('products', static function ($query) use ($productId) { + $query->where('product_id', $productId); + })->get(); + + return $this->handleResults($capacityAssignments, CapacityAssignmentDomainObject::class); + } + + public function addTaxesAndFeesToProduct(int $productId, array $taxIds): void + { + Product::findOrFail($productId)?->tax_and_fees()->sync($taxIds); + } + + public function addCapacityAssignmentToProducts(int $capacityAssignmentId, array $productIds): void + { + $productIds = array_unique($productIds); + + Product::whereNotIn('id', $productIds) + ->whereHas('capacity_assignments', function ($query) use ($capacityAssignmentId) { + $query->where('capacity_assignment_id', $capacityAssignmentId); + }) + ->each(function (Product $product) use ($capacityAssignmentId) { + $product->capacity_assignments()->detach($capacityAssignmentId); + }); + + Product::whereIn('id', $productIds) + ->each(function (Product $product) use ($capacityAssignmentId) { + $product->capacity_assignments()->syncWithoutDetaching([$capacityAssignmentId]); + }); + } + + public function addCheckInListToProducts(int $checkInListId, array $productIds): void + { + $productIds = array_unique($productIds); + + Product::whereNotIn('id', $productIds) + ->whereHas('check_in_lists', function ($query) use ($checkInListId) { + $query->where('check_in_list_id', $checkInListId); + }) + ->each(function (Product $product) use ($checkInListId) { + $product->check_in_lists()->detach($checkInListId); + }); + + Product::whereIn('id', $productIds) + ->each(function (Product $product) use ($checkInListId) { + $product->check_in_lists()->syncWithoutDetaching([$checkInListId]); + }); + } + + public function removeCheckInListFromProducts(int $checkInListId): void + { + $checkInList = CheckInList::find($checkInListId); + + $checkInList?->products()->detach(); + } + + public function removeCapacityAssignmentFromProducts(int $capacityAssignmentId): void + { + $capacityAssignment = CapacityAssignment::find($capacityAssignmentId); + + $capacityAssignment?->products()->detach(); + } + + /** + * @throws Throwable + */ + public function bulkUpdateProductsAndCategories(int $eventId, array $productUpdates, array $categoryUpdates): void + { + $this->db->beginTransaction(); + + try { + $productIds = array_column($productUpdates, 'id'); + $productOrders = range(1, count($productUpdates)); + $productCategoryIds = array_column($productUpdates, 'product_category_id'); + + $productParameters = [ + 'eventId' => $eventId, + 'productIds' => '{' . implode(',', $productIds) . '}', + 'productOrders' => '{' . implode(',', $productOrders) . '}', + 'productCategoryIds' => '{' . implode(',', $productCategoryIds) . '}', + ]; + + $productUpdateQuery = "WITH new_order AS ( + SELECT unnest(:productIds::bigint[]) AS product_id, + unnest(:productOrders::int[]) AS order, + unnest(:productCategoryIds::bigint[]) AS category_id + ) + UPDATE products + SET \"order\" = new_order.order, + product_category_id = new_order.category_id, + updated_at = NOW() + FROM new_order + WHERE products.id = new_order.product_id AND products.event_id = :eventId"; + + $this->db->update($productUpdateQuery, $productParameters); + + $categoryIds = array_column($categoryUpdates, 'id'); + $categoryOrders = array_column($categoryUpdates, 'order'); + + $categoryParameters = [ + 'eventId' => $eventId, + 'categoryIds' => '{' . implode(',', $categoryIds) . '}', + 'categoryOrders' => '{' . implode(',', $categoryOrders) . '}', + ]; + + $categoryUpdateQuery = "WITH new_category_order AS ( + SELECT unnest(:categoryIds::bigint[]) AS category_id, + unnest(:categoryOrders::int[]) AS order + ) + UPDATE product_categories + SET \"order\" = new_category_order.order, + updated_at = NOW() + FROM new_category_order + WHERE product_categories.id = new_category_order.category_id AND product_categories.event_id = :eventId"; + + $this->db->update($categoryUpdateQuery, $categoryParameters); + + $this->db->commit(); + } catch (Exception $e) { + $this->db->rollBack(); + throw $e; + } + } + + public function hasAssociatedOrders(int $productId): bool + { + return $this->db->table('order_items') + ->join('orders', 'order_items.order_id', '=', 'orders.id') + ->whereIn('orders.status', [OrderStatus::COMPLETED->name, OrderStatus::CANCELLED->name]) + ->where('order_items.product_id', $productId) + ->exists(); + } + + public function getModel(): string + { + return Product::class; + } + + public function getDomainObject(): string + { + return ProductDomainObject::class; + } +} diff --git a/backend/app/Repository/Eloquent/QuestionRepository.php b/backend/app/Repository/Eloquent/QuestionRepository.php index 8c2e1e9a..1e0dcdbb 100644 --- a/backend/app/Repository/Eloquent/QuestionRepository.php +++ b/backend/app/Repository/Eloquent/QuestionRepository.php @@ -5,21 +5,21 @@ use HiEvents\DomainObjects\Generated\QuestionDomainObjectAbstract; use HiEvents\DomainObjects\QuestionDomainObject; use HiEvents\Models\Question; -use HiEvents\Models\TicketQuestion; +use HiEvents\Models\ProductQuestion; use HiEvents\Repository\Interfaces\QuestionRepositoryInterface; -use HiEvents\Repository\Interfaces\TicketRepositoryInterface; +use HiEvents\Repository\Interfaces\ProductRepositoryInterface; use Illuminate\Database\DatabaseManager; use Illuminate\Foundation\Application; use Illuminate\Support\Collection; class QuestionRepository extends BaseRepository implements QuestionRepositoryInterface { - private TicketRepositoryInterface $ticketRepository; + private ProductRepositoryInterface $productRepository; - public function __construct(Application $application, DatabaseManager $db, TicketRepositoryInterface $ticketRepository) + public function __construct(Application $application, DatabaseManager $db, ProductRepositoryInterface $productRepository) { parent::__construct($application, $db); - $this->ticketRepository = $ticketRepository; + $this->productRepository = $productRepository; } protected function getModel(): string @@ -32,37 +32,37 @@ public function getDomainObject(): string return QuestionDomainObject::class; } - public function create(array $attributes, array $ticketIds = []): QuestionDomainObject + public function create(array $attributes, array $productIds = []): QuestionDomainObject { /** @var QuestionDomainObject $question */ $question = parent::create($attributes); - foreach ($ticketIds as $ticketId) { - $ticketQuestion = new TicketQuestion(); - $ticketQuestion->create([ - 'ticket_id' => $ticketId, + foreach ($productIds as $productId) { + $productQuestion = new ProductQuestion(); + $productQuestion->create([ + 'product_id' => $productId, 'question_id' => $question->getId(), ]); } - $question->setTickets($this->ticketRepository->findWhereIn('id', $ticketIds)); + $question->setProducts($this->productRepository->findWhereIn('id', $productIds)); return $question; } - public function updateQuestion(int $questionId, int $eventId, array $attributes, array $ticketIds = []): void + public function updateQuestion(int $questionId, int $eventId, array $attributes, array $productIds = []): void { $this->updateWhere($attributes, [ 'id' => $questionId, 'event_id' => $eventId, ]); - TicketQuestion::where('question_id', $questionId)->delete(); + ProductQuestion::where('question_id', $questionId)->delete(); - foreach ($ticketIds as $ticketId) { - $ticketQuestion = new TicketQuestion(); - $ticketQuestion->create([ - 'ticket_id' => $ticketId, + foreach ($productIds as $productId) { + $productQuestion = new ProductQuestion(); + $productQuestion->create([ + 'product_id' => $productId, 'question_id' => $questionId, ]); } diff --git a/backend/app/Repository/Eloquent/TicketPriceRepository.php b/backend/app/Repository/Eloquent/TicketPriceRepository.php deleted file mode 100644 index 9f405801..00000000 --- a/backend/app/Repository/Eloquent/TicketPriceRepository.php +++ /dev/null @@ -1,20 +0,0 @@ -query)) { - $where[] = static function (Builder $builder) use ($params) { - $builder - ->where(TicketDomainObjectAbstract::TITLE, 'ilike', '%' . $params->query . '%'); - }; - } - - $this->model = $this->model->orderBy( - $params->sort_by ?? TicketDomainObject::getDefaultSort(), - $params->sort_direction ?? TicketDomainObject::getDefaultSortDirection(), - ); - - return $this->paginateWhere( - where: $where, - limit: $params->per_page, - page: $params->page, - ); - } - - /** - * @param int $ticketId - * @param int $ticketPriceId - * @return int - */ - public function getQuantityRemainingForTicketPrice(int $ticketId, int $ticketPriceId): int - { - $query = <<db->selectOne($query, [ - 'ticketPriceId' => $ticketPriceId, - 'ticketId' => $ticketId - ]); - - if ($result === null) { - throw new RuntimeException('Ticket price not found'); - } - - if ($result->unlimited_tickets_available) { - return Constants::INFINITE; - } - - return (int)$result->quantity_remaining; - } - - public function getTaxesByTicketId(int $ticketId): Collection - { - $query = <<db->select($query, [ - 'ticketId' => $ticketId - ]); - - return $this->handleResults($taxAndFees, TaxAndFeesDomainObject::class); - } - - public function getTicketsByTaxId(int $taxId): Collection - { - $query = <<model->select($query, [ - 'taxAndFeeId' => $taxId - ]); - - return $this->handleResults($tickets, TicketDomainObject::class); - } - - public function getCapacityAssignmentsByTicketId(int $ticketId): Collection - { - $capacityAssignments = CapacityAssignment::whereHas('tickets', static function ($query) use ($ticketId) { - $query->where('ticket_id', $ticketId); - })->get(); - - return $this->handleResults($capacityAssignments, CapacityAssignmentDomainObject::class); - } - - public function addTaxesAndFeesToTicket(int $ticketId, array $taxIds): void - { - Ticket::findOrFail($ticketId)?->tax_and_fees()->sync($taxIds); - } - - public function addCapacityAssignmentToTickets(int $capacityAssignmentId, array $ticketIds): void - { - $ticketIds = array_unique($ticketIds); - - Ticket::whereNotIn('id', $ticketIds) - ->whereHas('capacity_assignments', function ($query) use ($capacityAssignmentId) { - $query->where('capacity_assignment_id', $capacityAssignmentId); - }) - ->each(function (Ticket $ticket) use ($capacityAssignmentId) { - $ticket->capacity_assignments()->detach($capacityAssignmentId); - }); - - Ticket::whereIn('id', $ticketIds) - ->each(function (Ticket $ticket) use ($capacityAssignmentId) { - $ticket->capacity_assignments()->syncWithoutDetaching([$capacityAssignmentId]); - }); - } - - public function addCheckInListToTickets(int $checkInListId, array $ticketIds): void - { - $ticketIds = array_unique($ticketIds); - - Ticket::whereNotIn('id', $ticketIds) - ->whereHas('check_in_lists', function ($query) use ($checkInListId) { - $query->where('check_in_list_id', $checkInListId); - }) - ->each(function (Ticket $ticket) use ($checkInListId) { - $ticket->check_in_lists()->detach($checkInListId); - }); - - Ticket::whereIn('id', $ticketIds) - ->each(function (Ticket $ticket) use ($checkInListId) { - $ticket->check_in_lists()->syncWithoutDetaching([$checkInListId]); - }); - } - - public function removeCheckInListFromTickets(int $checkInListId): void - { - $checkInList = CheckInList::find($checkInListId); - - $checkInList?->tickets()->detach(); - } - - public function removeCapacityAssignmentFromTickets(int $capacityAssignmentId): void - { - $capacityAssignment = CapacityAssignment::find($capacityAssignmentId); - - $capacityAssignment?->tickets()->detach(); - } - - public function sortTickets(int $eventId, array $orderedTicketIds): void - { - $parameters = [ - 'eventId' => $eventId, - 'ticketIds' => '{' . implode(',', $orderedTicketIds) . '}', - 'orders' => '{' . implode(',', range(1, count($orderedTicketIds))) . '}', - ]; - - $query = "WITH new_order AS ( - SELECT unnest(:ticketIds::bigint[]) AS ticket_id, - unnest(:orders::int[]) AS order - ) - UPDATE tickets - SET \"order\" = new_order.order - FROM new_order - WHERE tickets.id = new_order.ticket_id AND tickets.event_id = :eventId"; - - $this->db->update($query, $parameters); - } - - public function getModel(): string - { - return Ticket::class; - } - - public function getDomainObject(): string - { - return TicketDomainObject::class; - } -} diff --git a/backend/app/Repository/Eloquent/Value/OrderAndDirection.php b/backend/app/Repository/Eloquent/Value/OrderAndDirection.php new file mode 100644 index 00000000..4a6058ef --- /dev/null +++ b/backend/app/Repository/Eloquent/Value/OrderAndDirection.php @@ -0,0 +1,36 @@ +validate(); + } + + public function getOrder(): string + { + return $this->order; + } + + public function getDirection(): string + { + return $this->direction; + } + + private function validate(): void + { + if (!in_array($this->direction, ['asc', 'desc'])) { + throw new InvalidArgumentException(__('Invalid direction. Must be either asc or desc')); + } + } +} diff --git a/backend/app/Repository/Eloquent/Value/Relationship.php b/backend/app/Repository/Eloquent/Value/Relationship.php index 857173ca..6dfcb761 100644 --- a/backend/app/Repository/Eloquent/Value/Relationship.php +++ b/backend/app/Repository/Eloquent/Value/Relationship.php @@ -2,18 +2,27 @@ namespace HiEvents\Repository\Eloquent\Value; -readonly class Relationship +use HiEvents\DomainObjects\Interfaces\DomainObjectInterface; +use InvalidArgumentException; + +class Relationship { public function __construct( - private string $domainObject, + private readonly string $domainObject, /** * @var Relationship[]|null */ - private ?array $nested = [], + private readonly ?array $nested = [], + + private readonly ?string $name = null, - private ?string $name = null, + /** + * @var OrderAndDirection[] + */ + private readonly array $orderAndDirections = [], ) { + $this->validate(); } public function getName(): string @@ -31,13 +40,23 @@ public function getDomainObject(): string return $this->domainObject; } + public function getOrderAndDirections(): array + { + return $this->orderAndDirections; + } + public function buildLaravelEagerLoadArray(): array { - if (!$this->nested) { - return [$this->getName()]; + $results = [ + $this->getName() => $this->buildOrderAndDirectionEloquentCallback() + ]; + + // If there are nested relationships, build them and merge into the results array + if ($this->nested) { + $results = array_merge($results, $this->buildNested($this, '')); } - return $this->buildNested($this, ''); + return $results; } private function buildNested(Relationship $relationship, string $prefix): array @@ -47,11 +66,51 @@ private function buildNested(Relationship $relationship, string $prefix): array if ($relationship->nested) { foreach ($relationship->nested as $nested) { $nestedPrefix = $prefix === '' ? $relationship->getName() : $prefix . '.' . $relationship->getName(); - $results[] = $nestedPrefix . '.' . $nested->getName(); + $results[$nestedPrefix . '.' . $nested->getName()] = $nested->buildOrderAndDirectionEloquentCallback(); $results = array_merge($results, $this->buildNested($nested, $nestedPrefix)); } } return $results; } + + private function buildOrderAndDirectionEloquentCallback(): callable|array + { + if ($this->getOrderAndDirections() === []) { + return []; + } + + return function ($query) { + foreach ($this->orderAndDirections as $orderAndDirection) { + $query->orderBy($orderAndDirection->getOrder(), $orderAndDirection->getDirection()); + } + }; + } + + private function validate(): void + { + if (!is_subclass_of($this->domainObject, DomainObjectInterface::class)) { + throw new InvalidArgumentException( + __('DomainObject must be a valid :interface.', [ + 'interface' => DomainObjectInterface::class, + ]), + ); + } + + foreach ($this->nested as $nested) { + if (!is_a($nested, __CLASS__)) { + throw new InvalidArgumentException( + __('Nested relationships must be an array of Relationship objects.'), + ); + } + } + + foreach ($this->orderAndDirections as $orderAndDirection) { + if (!is_a($orderAndDirection, OrderAndDirection::class)) { + throw new InvalidArgumentException( + __('OrderAndDirections must be an array of OrderAndDirection objects.'), + ); + } + } + } } diff --git a/backend/app/Repository/Interfaces/ProductCategoryRepositoryInterface.php b/backend/app/Repository/Interfaces/ProductCategoryRepositoryInterface.php new file mode 100644 index 00000000..593fb5d7 --- /dev/null +++ b/backend/app/Repository/Interfaces/ProductCategoryRepositoryInterface.php @@ -0,0 +1,18 @@ + + */ +interface ProductCategoryRepositoryInterface extends RepositoryInterface +{ + public function findByEventId(int $eventId, QueryParamsDTO $queryParamsDTO): Collection; + + public function getNextOrder(int $eventId); +} diff --git a/backend/app/Repository/Interfaces/ProductPriceRepositoryInterface.php b/backend/app/Repository/Interfaces/ProductPriceRepositoryInterface.php new file mode 100644 index 00000000..23b538f1 --- /dev/null +++ b/backend/app/Repository/Interfaces/ProductPriceRepositoryInterface.php @@ -0,0 +1,15 @@ + + */ +interface ProductPriceRepositoryInterface extends RepositoryInterface +{ +} diff --git a/backend/app/Repository/Interfaces/ProductRepositoryInterface.php b/backend/app/Repository/Interfaces/ProductRepositoryInterface.php new file mode 100644 index 00000000..d484fb0c --- /dev/null +++ b/backend/app/Repository/Interfaces/ProductRepositoryInterface.php @@ -0,0 +1,93 @@ + + */ +interface ProductRepositoryInterface extends RepositoryInterface +{ + /** + * @param int $eventId + * @param QueryParamsDTO $params + * @return LengthAwarePaginator + */ + public function findByEventId(int $eventId, QueryParamsDTO $params): LengthAwarePaginator; + + /** + * @param int $productId + * @param int $productPriceId + * @return int + */ + public function getQuantityRemainingForProductPrice(int $productId, int $productPriceId): int; + + /** + * @param int $productId + * @return Collection + */ + public function getTaxesByProductId(int $productId): Collection; + + /** + * @param int $taxId + * @return Collection + */ + public function getProductsByTaxId(int $taxId): Collection; + + /** + * @param int $productId + * @return Collection + */ + public function getCapacityAssignmentsByProductId(int $productId): Collection; + + /** + * @param int $productId + * @param array $taxIds + * @return void + */ + public function addTaxesAndFeesToProduct(int $productId, array $taxIds): void; + + /** + * @param array $productIds + * @param int $capacityAssignmentId + * @return void + */ + public function addCapacityAssignmentToProducts(int $capacityAssignmentId, array $productIds): void; + + /** + * @param int $checkInListId + * @param array $productIds + * @return void + */ + public function addCheckInListToProducts(int $checkInListId, array $productIds): void; + + /** + * @param int $checkInListId + * @return void + */ + public function removeCheckInListFromProducts(int $checkInListId): void; + + /** + * @param int $capacityAssignmentId + * @return void + */ + public function removeCapacityAssignmentFromProducts(int $capacityAssignmentId): void; + + + /** + * @param int $eventId + * @param array $productUpdates + * @param array $categoryUpdates + * @return void + */ + public function bulkUpdateProductsAndCategories(int $eventId, array $productUpdates, array $categoryUpdates): void; + + public function hasAssociatedOrders(int $productId): bool; +} diff --git a/backend/app/Repository/Interfaces/QuestionRepositoryInterface.php b/backend/app/Repository/Interfaces/QuestionRepositoryInterface.php index 6a5438de..61c303e6 100644 --- a/backend/app/Repository/Interfaces/QuestionRepositoryInterface.php +++ b/backend/app/Repository/Interfaces/QuestionRepositoryInterface.php @@ -13,9 +13,9 @@ interface QuestionRepositoryInterface extends RepositoryInterface { public function findByEventId(int $eventId): Collection; - public function create(array $attributes, array $ticketIds = []): QuestionDomainObject; + public function create(array $attributes, array $productIds = []): QuestionDomainObject; - public function updateQuestion(int $questionId, int $eventId, array $attributes, array $ticketIds = []): void; + public function updateQuestion(int $questionId, int $eventId, array $attributes, array $productIds = []): void; public function sortQuestions(int $eventId, array $orderedQuestionIds): void; } diff --git a/backend/app/Repository/Interfaces/RepositoryInterface.php b/backend/app/Repository/Interfaces/RepositoryInterface.php index 5481dbc8..dfa9c2c7 100644 --- a/backend/app/Repository/Interfaces/RepositoryInterface.php +++ b/backend/app/Repository/Interfaces/RepositoryInterface.php @@ -4,6 +4,7 @@ use Exception; use HiEvents\DomainObjects\Interfaces\DomainObjectInterface; +use HiEvents\Repository\Eloquent\Value\OrderAndDirection; use Illuminate\Contracts\Pagination\Paginator; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Pagination\LengthAwarePaginator; @@ -17,6 +18,9 @@ interface RepositoryInterface /** @var array */ public const DEFAULT_COLUMNS = ['*']; + /** @var string */ + public const DEFAULT_ORDER_DIRECTION = 'asc'; + /** @var int */ public const DEFAULT_PAGINATE_LIMIT = 20; @@ -100,9 +104,15 @@ public function findFirst(int $id, array $columns = self::DEFAULT_COLUMNS): ?Dom /** * @param array $where * @param array $columns + * @param OrderAndDirection[] $orderAndDirections * @return Collection */ - public function findWhere(array $where, array $columns = self::DEFAULT_COLUMNS): Collection; + public function findWhere( + array $where, + array $columns = self::DEFAULT_COLUMNS, + /** @var OrderAndDirection[] */ + array $orderAndDirections = [], + ): Collection; /** * @param array $where diff --git a/backend/app/Repository/Interfaces/TicketPriceRepositoryInterface.php b/backend/app/Repository/Interfaces/TicketPriceRepositoryInterface.php deleted file mode 100644 index 83df3a0b..00000000 --- a/backend/app/Repository/Interfaces/TicketPriceRepositoryInterface.php +++ /dev/null @@ -1,15 +0,0 @@ - - */ -interface TicketPriceRepositoryInterface extends RepositoryInterface -{ -} diff --git a/backend/app/Repository/Interfaces/TicketRepositoryInterface.php b/backend/app/Repository/Interfaces/TicketRepositoryInterface.php deleted file mode 100644 index 4143956a..00000000 --- a/backend/app/Repository/Interfaces/TicketRepositoryInterface.php +++ /dev/null @@ -1,89 +0,0 @@ - - */ -interface TicketRepositoryInterface extends RepositoryInterface -{ - /** - * @param int $eventId - * @param QueryParamsDTO $params - * @return LengthAwarePaginator - */ - public function findByEventId(int $eventId, QueryParamsDTO $params): LengthAwarePaginator; - - /** - * @param int $ticketId - * @param int $ticketPriceId - * @return int - */ - public function getQuantityRemainingForTicketPrice(int $ticketId, int $ticketPriceId): int; - - /** - * @param int $ticketId - * @return Collection - */ - public function getTaxesByTicketId(int $ticketId): Collection; - - /** - * @param int $taxId - * @return Collection - */ - public function getTicketsByTaxId(int $taxId): Collection; - - /** - * @param int $ticketId - * @return Collection - */ - public function getCapacityAssignmentsByTicketId(int $ticketId): Collection; - - /** - * @param int $ticketId - * @param array $taxIds - * @return void - */ - public function addTaxesAndFeesToTicket(int $ticketId, array $taxIds): void; - - /** - * @param array $ticketIds - * @param int $capacityAssignmentId - * @return void - */ - public function addCapacityAssignmentToTickets(int $capacityAssignmentId, array $ticketIds): void; - - /** - * @param int $checkInListId - * @param array $ticketIds - * @return void - */ - public function addCheckInListToTickets(int $checkInListId, array $ticketIds): void; - - /** - * @param int $checkInListId - * @return void - */ - public function removeCheckInListFromTickets(int $checkInListId): void; - - /** - * @param int $capacityAssignmentId - * @return void - */ - public function removeCapacityAssignmentFromTickets(int $capacityAssignmentId): void; - - /** - * @param int $eventId - * @param array $orderedTicketIds - * @return void - */ - public function sortTickets(int $eventId, array $orderedTicketIds): void; -} diff --git a/backend/app/Resources/Account/Stripe/StripeConnectAccountResponseResource.php b/backend/app/Resources/Account/Stripe/StripeConnectAccountResponseResource.php index 1f006d9d..a31d2eac 100644 --- a/backend/app/Resources/Account/Stripe/StripeConnectAccountResponseResource.php +++ b/backend/app/Resources/Account/Stripe/StripeConnectAccountResponseResource.php @@ -3,7 +3,7 @@ namespace HiEvents\Resources\Account\Stripe; use HiEvents\Resources\Account\AccountResource; -use HiEvents\Services\Handlers\Account\Payment\Stripe\DTO\CreateStripeConnectAccountResponse; +use HiEvents\Services\Application\Handlers\Account\Payment\Stripe\DTO\CreateStripeConnectAccountResponse; use Illuminate\Http\Resources\Json\JsonResource; /** diff --git a/backend/app/Resources/Attendee/AttendeeResource.php b/backend/app/Resources/Attendee/AttendeeResource.php index 819ea89a..e1af56f8 100644 --- a/backend/app/Resources/Attendee/AttendeeResource.php +++ b/backend/app/Resources/Attendee/AttendeeResource.php @@ -7,7 +7,7 @@ use HiEvents\Resources\CheckInList\AttendeeCheckInResource; use HiEvents\Resources\Order\OrderResource; use HiEvents\Resources\Question\QuestionAnswerViewResource; -use HiEvents\Resources\Ticket\TicketResource; +use HiEvents\Resources\Product\ProductResource; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; @@ -21,8 +21,8 @@ public function toArray(Request $request): array return [ 'id' => $this->getId(), 'order_id' => $this->getOrderId(), - 'ticket_id' => $this->getTicketId(), - 'ticket_price_id' => $this->getTicketPriceId(), + 'product_id' => $this->getProductId(), + 'product_price_id' => $this->getProductPriceId(), 'event_id' => $this->getEventId(), 'email' => $this->getEmail(), 'status' => $this->getStatus(), @@ -31,14 +31,15 @@ public function toArray(Request $request): array 'public_id' => $this->getPublicId(), 'short_id' => $this->getShortId(), 'locale' => $this->getLocale(), + 'notes' => $this->getNotes(), + 'product' => $this->when( + !is_null($this->getProduct()), + fn() => new ProductResource($this->getProduct()), + ), 'check_in' => $this->when( condition: $this->getCheckIn() !== null, value: fn() => new AttendeeCheckInResource($this->getCheckIn()), ), - 'ticket' => $this->when( - condition: !is_null($this->getTicket()), - value: fn() => new TicketResource($this->getTicket()), - ), 'order' => $this->when( condition: !is_null($this->getOrder()), value: fn() => new OrderResource($this->getOrder()) @@ -47,10 +48,9 @@ public function toArray(Request $request): array condition: $this->getQuestionAndAnswerViews() !== null, value: fn() => QuestionAnswerViewResource::collection( $this->getQuestionAndAnswerViews() - ?->filter(fn($qav) => $qav->getBelongsTo() === QuestionBelongsTo::TICKET->name) + ?->filter(fn($qav) => $qav->getBelongsTo() === QuestionBelongsTo::PRODUCT->name) ) ), - 'created_at' => $this->getCreatedAt(), 'updated_at' => $this->getUpdatedAt(), ]; diff --git a/backend/app/Resources/Attendee/AttendeeResourcePublic.php b/backend/app/Resources/Attendee/AttendeeResourcePublic.php index 8c40ffcd..1f4ffc1a 100644 --- a/backend/app/Resources/Attendee/AttendeeResourcePublic.php +++ b/backend/app/Resources/Attendee/AttendeeResourcePublic.php @@ -3,7 +3,7 @@ namespace HiEvents\Resources\Attendee; use HiEvents\DomainObjects\AttendeeDomainObject; -use HiEvents\Resources\Ticket\TicketMinimalResourcePublic; +use HiEvents\Resources\Product\ProductMinimalResourcePublic; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; @@ -22,9 +22,9 @@ public function toArray(Request $request): array 'last_name' => $this->getLastName(), 'public_id' => $this->getPublicId(), 'short_id' => $this->getShortId(), - 'ticket_id' => $this->getTicketId(), - 'ticket_price_id' => $this->getTicketPriceId(), - 'ticket' => $this->when((bool)$this->getTicket(), fn() => new TicketMinimalResourcePublic($this->getTicket())), + 'product_id' => $this->getProductId(), + 'product_price_id' => $this->getProductPriceId(), + 'product' => $this->when((bool)$this->getProduct(), fn() => new ProductMinimalResourcePublic($this->getProduct())), 'locale' => $this->getLocale(), ]; } diff --git a/backend/app/Resources/Attendee/AttendeeWithCheckInPublicResource.php b/backend/app/Resources/Attendee/AttendeeWithCheckInPublicResource.php index 9750f8c1..ee8b888e 100644 --- a/backend/app/Resources/Attendee/AttendeeWithCheckInPublicResource.php +++ b/backend/app/Resources/Attendee/AttendeeWithCheckInPublicResource.php @@ -20,8 +20,8 @@ public function toArray(Request $request): array 'first_name' => $this->getFirstName(), 'last_name' => $this->getLastName(), 'public_id' => $this->getPublicId(), - 'ticket_id' => $this->getTicketId(), - 'ticket_price_id' => $this->getTicketPriceId(), + 'product_id' => $this->getProductId(), + 'product_price_id' => $this->getProductPriceId(), 'locale' => $this->getLocale(), $this->mergeWhen($this->getCheckIn() !== null, [ 'check_in' => new AttendeeCheckInPublicResource($this->getCheckIn()), diff --git a/backend/app/Resources/Auth/AuthenticatedResponseResource.php b/backend/app/Resources/Auth/AuthenticatedResponseResource.php index f1603441..0d1cfa65 100644 --- a/backend/app/Resources/Auth/AuthenticatedResponseResource.php +++ b/backend/app/Resources/Auth/AuthenticatedResponseResource.php @@ -4,7 +4,7 @@ use HiEvents\Resources\Account\AccountResource; use HiEvents\Resources\User\UserResource; -use HiEvents\Services\Handlers\Auth\DTO\AuthenticatedResponseDTO; +use HiEvents\Services\Application\Handlers\Auth\DTO\AuthenticatedResponseDTO; use Illuminate\Http\Resources\Json\JsonResource; /** diff --git a/backend/app/Resources/CapacityAssignment/CapacityAssignmentResource.php b/backend/app/Resources/CapacityAssignment/CapacityAssignmentResource.php index af6af462..c35c6ef3 100644 --- a/backend/app/Resources/CapacityAssignment/CapacityAssignmentResource.php +++ b/backend/app/Resources/CapacityAssignment/CapacityAssignmentResource.php @@ -4,7 +4,7 @@ use HiEvents\DomainObjects\CapacityAssignmentDomainObject; use HiEvents\DomainObjects\Enums\CapacityAssignmentAppliesTo; -use HiEvents\DomainObjects\TicketDomainObject; +use HiEvents\DomainObjects\ProductDomainObject; use HiEvents\Resources\BaseResource; use Illuminate\Http\Request; @@ -25,11 +25,11 @@ public function toArray(Request $request): array 'status' => $this->getStatus(), 'event_id' => $this->getEventId(), $this->mergeWhen( - condition: $this->getTickets() !== null && $this->getAppliesTo() === CapacityAssignmentAppliesTo::TICKETS->name, + condition: $this->getProducts() !== null && $this->getAppliesTo() === CapacityAssignmentAppliesTo::PRODUCTS->name, value: [ - 'tickets' => $this->getTickets()?->map(fn(TicketDomainObject $ticket) => [ - 'id' => $ticket->getId(), - 'title' => $ticket->getTitle(), + 'products' => $this->getProducts()?->map(fn(ProductDomainObject $product) => [ + 'id' => $product->getId(), + 'title' => $product->getTitle(), ]), ]), ]; diff --git a/backend/app/Resources/CheckInList/AttendeeCheckInResource.php b/backend/app/Resources/CheckInList/AttendeeCheckInResource.php index 15246015..aa03b45d 100644 --- a/backend/app/Resources/CheckInList/AttendeeCheckInResource.php +++ b/backend/app/Resources/CheckInList/AttendeeCheckInResource.php @@ -16,7 +16,7 @@ public function toArray($request): array 'id' => $this->getId(), 'attendee_id' => $this->getAttendeeId(), 'check_in_list_id' => $this->getCheckInListId(), - 'ticket_id' => $this->getTicketId(), + 'product_id' => $this->getProductId(), 'event_id' => $this->getEventId(), 'short_id' => $this->getShortId(), 'created_at' => $this->getCreatedAt(), diff --git a/backend/app/Resources/CheckInList/CheckInListResource.php b/backend/app/Resources/CheckInList/CheckInListResource.php index 51d96902..744f947c 100644 --- a/backend/app/Resources/CheckInList/CheckInListResource.php +++ b/backend/app/Resources/CheckInList/CheckInListResource.php @@ -3,7 +3,7 @@ namespace HiEvents\Resources\CheckInList; use HiEvents\DomainObjects\CheckInListDomainObject; -use HiEvents\Resources\Ticket\TicketResource; +use HiEvents\Resources\Product\ProductResource; use Illuminate\Http\Resources\Json\JsonResource; /** @@ -26,8 +26,8 @@ public function toArray($request): array 'is_expired' => $this->isExpired($this->getEvent()->getTimezone()), 'is_active' => $this->isActivated($this->getEvent()->getTimezone()), ]), - $this->mergeWhen($this->getTickets() !== null, fn() => [ - 'tickets' => TicketResource::collection($this->getTickets()), + $this->mergeWhen($this->getProducts() !== null, fn() => [ + 'products' => ProductResource::collection($this->getProducts()), ]), ]; } diff --git a/backend/app/Resources/CheckInList/CheckInListResourcePublic.php b/backend/app/Resources/CheckInList/CheckInListResourcePublic.php index 04668700..7135da87 100644 --- a/backend/app/Resources/CheckInList/CheckInListResourcePublic.php +++ b/backend/app/Resources/CheckInList/CheckInListResourcePublic.php @@ -4,7 +4,7 @@ use HiEvents\DomainObjects\CheckInListDomainObject; use HiEvents\Resources\Event\EventResourcePublic; -use HiEvents\Resources\Ticket\TicketMinimalResourcePublic; +use HiEvents\Resources\Product\ProductMinimalResourcePublic; use Illuminate\Http\Resources\Json\JsonResource; /** @@ -28,8 +28,8 @@ public function toArray($request): array 'is_active' => $this->isActivated($this->getEvent()->getTimezone()), 'event' => EventResourcePublic::make($this->getEvent()), ]), - $this->mergeWhen($this->getTickets() !== null, fn() => [ - 'tickets' => TicketMinimalResourcePublic::collection($this->getTickets()), + $this->mergeWhen($this->getProducts() !== null, fn() => [ + 'products' => ProductMinimalResourcePublic::collection($this->getProducts()), ]), ]; } diff --git a/backend/app/Resources/Event/EventResource.php b/backend/app/Resources/Event/EventResource.php index 7fcb425f..1a150524 100644 --- a/backend/app/Resources/Event/EventResource.php +++ b/backend/app/Resources/Event/EventResource.php @@ -6,7 +6,8 @@ use HiEvents\Resources\BaseResource; use HiEvents\Resources\Image\ImageResource; use HiEvents\Resources\Organizer\OrganizerResource; -use HiEvents\Resources\Ticket\TicketResource; +use HiEvents\Resources\Product\ProductResource; +use HiEvents\Resources\ProductCategory\ProductCategoryResource; use Illuminate\Http\Request; /** @@ -27,21 +28,28 @@ public function toArray(Request $request): array 'currency' => $this->getCurrency(), 'timezone' => $this->getTimezone(), 'slug' => $this->getSlug(), - 'tickets' => $this->when((bool)$this->getTickets(), fn() => TicketResource::collection($this->getTickets())), + 'products' => $this->when( + condition: (bool)$this->getProducts(), + value: fn() => ProductResource::collection($this->getProducts()), + ), + 'product_categories' => $this->when( + condition: (bool)$this->getProductCategories(), + value: fn() => ProductCategoryResource::collection($this->getProductCategories()), + ), 'attributes' => $this->when((bool)$this->getAttributes(), fn() => $this->getAttributes()), 'images' => $this->when((bool)$this->getImages(), fn() => ImageResource::collection($this->getImages())), 'location_details' => $this->when((bool)$this->getLocationDetails(), fn() => $this->getLocationDetails()), 'settings' => $this->when( - !is_null($this->getEventSettings()), - fn() => new EventSettingsResource($this->getEventSettings()) + condition: !is_null($this->getEventSettings()), + value: fn() => new EventSettingsResource($this->getEventSettings()) ), 'organizer' => $this->when( - !is_null($this->getOrganizer()), - fn() => new OrganizerResource($this->getOrganizer()) + condition: !is_null($this->getOrganizer()), + value: fn() => new OrganizerResource($this->getOrganizer()) ), 'statistics' => $this->when( - !is_null($this->getEventStatistics()), - fn() => new EventStatisticsResource($this->getEventStatistics()) + condition: !is_null($this->getEventStatistics()), + value: fn() => new EventStatisticsResource($this->getEventStatistics()) ), ]; } diff --git a/backend/app/Resources/Event/EventResourcePublic.php b/backend/app/Resources/Event/EventResourcePublic.php index d70dc0a1..4f70403a 100644 --- a/backend/app/Resources/Event/EventResourcePublic.php +++ b/backend/app/Resources/Event/EventResourcePublic.php @@ -6,8 +6,8 @@ use HiEvents\Resources\BaseResource; use HiEvents\Resources\Image\ImageResource; use HiEvents\Resources\Organizer\OrganizerResourcePublic; +use HiEvents\Resources\ProductCategory\ProductCategoryResourcePublic; use HiEvents\Resources\Question\QuestionResource; -use HiEvents\Resources\Ticket\TicketResourcePublic; use Illuminate\Http\Request; /** @@ -38,30 +38,29 @@ public function toArray(Request $request): array 'lifecycle_status' => $this->getLifecycleStatus(), 'timezone' => $this->getTimezone(), 'location_details' => $this->when((bool)$this->getLocationDetails(), fn() => $this->getLocationDetails()), - - 'tickets' => $this->when( - !is_null($this->getTickets()), - fn() => TicketResourcePublic::collection($this->getTickets()) + 'product_categories' => $this->when( + condition: !is_null($this->getProductCategories()) && $this->getProductCategories()->isNotEmpty(), + value: fn() => ProductCategoryResourcePublic::collection($this->getProductCategories()), ), 'settings' => $this->when( - !is_null($this->getEventSettings()), - fn() => new EventSettingsResourcePublic($this->getEventSettings(), $this->includePostCheckoutData), + condition: !is_null($this->getEventSettings()), + value: fn() => new EventSettingsResourcePublic($this->getEventSettings(), $this->includePostCheckoutData), ), // @TODO - public question resource 'questions' => $this->when( - !is_null($this->getQuestions()), - fn() => QuestionResource::collection($this->getQuestions()) + condition: !is_null($this->getQuestions()), + value: fn() => QuestionResource::collection($this->getQuestions()) ), 'attributes' => $this->when( - !is_null($this->getAttributes()), - fn() => collect($this->getAttributes())->reject(fn($attribute) => !$attribute['is_public'])), + condition: !is_null($this->getAttributes()), + value: fn() => collect($this->getAttributes())->reject(fn($attribute) => !$attribute['is_public'])), 'images' => $this->when( - !is_null($this->getImages()), - fn() => ImageResource::collection($this->getImages()) + condition: !is_null($this->getImages()), + value: fn() => ImageResource::collection($this->getImages()) ), 'organizer' => $this->when( - !is_null($this->getOrganizer()), - fn() => new OrganizerResourcePublic($this->getOrganizer()), + condition: !is_null($this->getOrganizer()), + value: fn() => new OrganizerResourcePublic($this->getOrganizer()), ), ]; } diff --git a/backend/app/Resources/Event/EventSettingsResource.php b/backend/app/Resources/Event/EventSettingsResource.php index 1886a27a..b1a62b97 100644 --- a/backend/app/Resources/Event/EventSettingsResource.php +++ b/backend/app/Resources/Event/EventSettingsResource.php @@ -15,7 +15,7 @@ public function toArray($request): array return [ 'pre_checkout_message' => $this->getPreCheckoutMessage(), 'post_checkout_message' => $this->getPostCheckoutMessage(), - 'ticket_page_message' => $this->getTicketPageMessage(), + 'product_page_message' => $this->getProductPageMessage(), 'continue_button_text' => $this->getContinueButtonText(), 'required_attendee_details' => $this->getRequireAttendeeDetails(), 'email_footer_message' => $this->getEmailFooterMessage(), diff --git a/backend/app/Resources/Event/EventSettingsResourcePublic.php b/backend/app/Resources/Event/EventSettingsResourcePublic.php index 397a1d2a..26bfca3f 100644 --- a/backend/app/Resources/Event/EventSettingsResourcePublic.php +++ b/backend/app/Resources/Event/EventSettingsResourcePublic.php @@ -28,7 +28,7 @@ public function toArray($request): array 'online_event_connection_details' => $this->getOnlineEventConnectionDetails(), ]), - 'ticket_page_message' => $this->getTicketPageMessage(), + 'product_page_message' => $this->getProductPageMessage(), 'continue_button_text' => $this->getContinueButtonText(), 'required_attendee_details' => $this->getRequireAttendeeDetails(), 'email_footer_message' => $this->getEmailFooterMessage(), diff --git a/backend/app/Resources/Event/EventStatisticsResource.php b/backend/app/Resources/Event/EventStatisticsResource.php index 91bd7d37..1b9d5c14 100644 --- a/backend/app/Resources/Event/EventStatisticsResource.php +++ b/backend/app/Resources/Event/EventStatisticsResource.php @@ -20,7 +20,8 @@ public function toArray(Request $request): array 'total_tax' => $this->getTotalTax(), 'sales_total_before_additions' => $this->getSalesTotalBeforeAdditions(), 'total_fee' => $this->getTotalFee(), - 'tickets_sold' => $this->getTicketsSold(), + 'products_sold' => $this->getProductsSold(), + 'attendees_registered' => $this->getAttendeesRegistered(), 'total_refunded' => $this->getTotalRefunded(), ]; } diff --git a/backend/app/Resources/Message/MessageResource.php b/backend/app/Resources/Message/MessageResource.php index 75dc30d6..b4b5ce81 100644 --- a/backend/app/Resources/Message/MessageResource.php +++ b/backend/app/Resources/Message/MessageResource.php @@ -22,7 +22,7 @@ public function toArray(Request $request): array 'type' => $this->getType(), 'attendee_ids' => $this->getAttendeeIds(), 'order_id' => $this->getOrderId(), - 'ticket_ids' => $this->getTicketIds(), + 'product_ids' => $this->getProductIds(), 'sent_at' => $this->getCreatedAt(), 'status' => $this->getStatus(), 'message_preview' => $this->getMessagePreview(), diff --git a/backend/app/Resources/Order/OrderItemResource.php b/backend/app/Resources/Order/OrderItemResource.php index 18e41559..3199ca49 100644 --- a/backend/app/Resources/Order/OrderItemResource.php +++ b/backend/app/Resources/Order/OrderItemResource.php @@ -19,7 +19,7 @@ public function toArray(Request $request): array 'total_before_additions' => $this->getTotalBeforeAdditions(), 'price' => $this->getPrice(), 'quantity' => $this->getQuantity(), - 'ticket_id' => $this->getTicketId(), + 'product_id' => $this->getProductId(), 'item_name' => $this->getItemName(), 'price_before_discount' => $this->getPriceBeforeDiscount(), 'taxes_and_fees_rollup' => $this->getTaxesAndFeesRollup(), diff --git a/backend/app/Resources/Order/OrderItemResourcePublic.php b/backend/app/Resources/Order/OrderItemResourcePublic.php index 471f762f..18ff0261 100644 --- a/backend/app/Resources/Order/OrderItemResourcePublic.php +++ b/backend/app/Resources/Order/OrderItemResourcePublic.php @@ -4,7 +4,7 @@ use HiEvents\DomainObjects\OrderItemDomainObject; use HiEvents\Resources\BaseResource; -use HiEvents\Resources\Ticket\TicketResourcePublic; +use HiEvents\Resources\Product\ProductResourcePublic; use Illuminate\Http\Request; /** @@ -22,14 +22,14 @@ public function toArray(Request $request): array 'price' => $this->getPrice(), 'price_before_discount' => $this->getPriceBeforeDiscount(), 'quantity' => $this->getQuantity(), - 'ticket_id' => $this->getTicketId(), - 'ticket_price_id' => $this->getTicketPriceId(), + 'product_id' => $this->getProductId(), + 'product_price_id' => $this->getProductPriceId(), 'item_name' => $this->getItemName(), 'total_service_fee' => $this->getTotalServiceFee(), 'total_tax' => $this->getTotalTax(), 'total_gross' => $this->getTotalGross(), 'taxes_and_fees_rollup' => $this->getTaxesAndFeesRollup(), - 'ticket' => $this->when((bool)$this->getTicket(), fn() => new TicketResourcePublic($this->getTicket())), + 'product' => $this->when((bool)$this->getProduct(), fn() => new ProductResourcePublic($this->getProduct())), ]; } } diff --git a/backend/app/Resources/Order/OrderResource.php b/backend/app/Resources/Order/OrderResource.php index b127d90d..cec43992 100644 --- a/backend/app/Resources/Order/OrderResource.php +++ b/backend/app/Resources/Order/OrderResource.php @@ -2,7 +2,6 @@ namespace HiEvents\Resources\Order; -use HiEvents\DomainObjects\Enums\QuestionBelongsTo; use HiEvents\DomainObjects\OrderDomainObject; use HiEvents\Resources\Attendee\AttendeeResource; use HiEvents\Resources\BaseResource; @@ -49,10 +48,7 @@ public function toArray(Request $request): array ), 'question_answers' => $this->when( !is_null($this->getQuestionAndAnswerViews()), - fn() => QuestionAnswerViewResource::collection( - $this->getQuestionAndAnswerViews() - ?->filter(fn($qav) => $qav->getBelongsTo() === QuestionBelongsTo::ORDER->name) - ) + fn() => QuestionAnswerViewResource::collection($this->getQuestionAndAnswerViews()), ), ]; } diff --git a/backend/app/Resources/Ticket/TicketMinimalResourcePublic.php b/backend/app/Resources/Product/ProductMinimalResourcePublic.php similarity index 51% rename from backend/app/Resources/Ticket/TicketMinimalResourcePublic.php rename to backend/app/Resources/Product/ProductMinimalResourcePublic.php index 10979bbd..daadaab8 100644 --- a/backend/app/Resources/Ticket/TicketMinimalResourcePublic.php +++ b/backend/app/Resources/Product/ProductMinimalResourcePublic.php @@ -1,15 +1,15 @@ $this->getType(), 'event_id' => $this->getEventId(), 'prices' => $this->when( - (bool)$this->getTicketPrices(), - fn() => TicketPriceResourcePublic::collection($this->getTicketPrices()), + (bool)$this->getProductPrices(), + fn() => ProductPriceResourcePublic::collection($this->getProductPrices()), ), + 'product_category_id' => $this->getProductCategoryId(), ]; } } diff --git a/backend/app/Resources/Ticket/TicketPriceResource.php b/backend/app/Resources/Product/ProductPriceResource.php similarity index 85% rename from backend/app/Resources/Ticket/TicketPriceResource.php rename to backend/app/Resources/Product/ProductPriceResource.php index 7a4844e3..3cbd945d 100644 --- a/backend/app/Resources/Ticket/TicketPriceResource.php +++ b/backend/app/Resources/Product/ProductPriceResource.php @@ -1,15 +1,15 @@ $this->getId(), 'title' => $this->getTitle(), 'type' => $this->getType(), + 'product_type' => $this->getProductType(), 'order' => $this->getOrder(), 'description' => $this->getDescription(), 'price' => $this->when( - $this->getType() !== TicketType::TIERED->name, + $this->getType() !== ProductPriceType::TIERED->name, fn() => $this->getPrice() ), - 'max_per_order' => $this->getMaxPerOrder() ?? self::DEFAULT_MAX_TICKETS, - 'min_per_order' => $this->getMinPerOrder() ?? self::DEFAULT_MIN_TICKETS, + 'max_per_order' => $this->getMaxPerOrder() ?? self::DEFAULT_MAX_PRODUCTS, + 'min_per_order' => $this->getMinPerOrder() ?? self::DEFAULT_MIN_PRODUCTS, 'quantity_sold' => $this->getQuantitySold(), 'sale_start_date' => $this->getSaleStartDate(), 'sale_end_date' => $this->getSaleEndDate(), @@ -46,7 +47,7 @@ public function toArray(Request $request): array 'is_before_sale_start_date' => $this->isBeforeSaleStartDate(), 'is_after_sale_end_date' => $this->isAfterSaleEndDate(), 'is_available' => $this->isAvailable(), - $this->mergeWhen((bool)$this->getTicketPrices(), fn() => [ + $this->mergeWhen((bool)$this->getProductPrices(), fn() => [ 'is_sold_out' => $this->isSoldOut(), ]), 'taxes_and_fees' => $this->when( @@ -54,9 +55,10 @@ public function toArray(Request $request): array fn() => TaxAndFeeResource::collection($this->getTaxAndFees()) ), 'prices' => $this->when( - (bool)$this->getTicketPrices(), - fn() => TicketPriceResource::collection($this->getTicketPrices()) + (bool)$this->getProductPrices(), + fn() => ProductPriceResource::collection($this->getProductPrices()) ), + 'product_category_id' => $this->getProductCategoryId(), ]; } } diff --git a/backend/app/Resources/Ticket/TicketResourcePublic.php b/backend/app/Resources/Product/ProductResourcePublic.php similarity index 68% rename from backend/app/Resources/Ticket/TicketResourcePublic.php rename to backend/app/Resources/Product/ProductResourcePublic.php index 788ed23c..4b8948dd 100644 --- a/backend/app/Resources/Ticket/TicketResourcePublic.php +++ b/backend/app/Resources/Product/ProductResourcePublic.php @@ -1,16 +1,16 @@ $this->getId(), 'title' => $this->getTitle(), 'type' => $this->getType(), + 'product_type' => $this->getProductType(), 'description' => $this->getDescription(), 'max_per_order' => $this->getMaxPerOrder(), 'min_per_order' => $this->getMinPerOrder(), @@ -31,23 +32,24 @@ public function toArray(Request $request): array 'quantity_available' => $this->getQuantityAvailable(), ]), 'price' => $this->when( - $this->getTicketPrices() && !$this->isTieredType(), + $this->getProductPrices() && !$this->isTieredType(), fn() => $this->getPrice(), ), 'prices' => $this->when( - (bool)$this->getTicketPrices(), - fn() => TicketPriceResourcePublic::collectionWithAdditionalData($this->getTicketPrices(), [ - TicketPriceResourcePublic::SHOW_QUANTITY_AVAILABLE => $this->getShowQuantityRemaining(), + (bool)$this->getProductPrices(), + fn() => ProductPriceResourcePublic::collectionWithAdditionalData($this->getProductPrices(), [ + ProductPriceResourcePublic::SHOW_QUANTITY_AVAILABLE => $this->getShowQuantityRemaining(), ]), ), 'taxes' => $this->when( (bool)$this->getTaxAndFees(), fn() => TaxAndFeeResource::collection($this->getTaxAndFees()) ), - $this->mergeWhen((bool)$this->getTicketPrices(), fn() => [ + $this->mergeWhen((bool)$this->getProductPrices(), fn() => [ 'is_available' => $this->isAvailable(), 'is_sold_out' => $this->isSoldOut(), ]), + 'product_category_id' => $this->getProductCategoryId(), ]; } } diff --git a/backend/app/Resources/ProductCategory/ProductCategoryResource.php b/backend/app/Resources/ProductCategory/ProductCategoryResource.php new file mode 100644 index 00000000..1d909545 --- /dev/null +++ b/backend/app/Resources/ProductCategory/ProductCategoryResource.php @@ -0,0 +1,28 @@ + $this->getId(), + 'name' => $this->getName(), + 'description' => $this->getDescription(), + 'is_hidden' => $this->getIsHidden(), + 'order' => $this->getOrder(), + 'no_products_message' => $this->getNoProductsMessage(), + $this->mergeWhen((bool)$this->getProducts(), fn() => [ + 'products' => ProductResource::collection($this->getProducts()), + ]), + ]; + } +} diff --git a/backend/app/Resources/ProductCategory/ProductCategoryResourcePublic.php b/backend/app/Resources/ProductCategory/ProductCategoryResourcePublic.php new file mode 100644 index 00000000..bfa087a5 --- /dev/null +++ b/backend/app/Resources/ProductCategory/ProductCategoryResourcePublic.php @@ -0,0 +1,28 @@ + $this->getId(), + 'name' => $this->getName(), + 'description' => $this->getDescription(), + 'is_hidden' => $this->getIsHidden(), + 'order' => $this->getOrder(), + 'no_products_message' => $this->getNoProductsMessage(), + $this->mergeWhen((bool)$this->getProducts(), fn() => [ + 'products' => ProductResourcePublic::collection($this->getProducts()), + ]), + ]; + } +} diff --git a/backend/app/Resources/PromoCode/PromoCodeResource.php b/backend/app/Resources/PromoCode/PromoCodeResource.php index 0aa9d80a..ec713886 100644 --- a/backend/app/Resources/PromoCode/PromoCodeResource.php +++ b/backend/app/Resources/PromoCode/PromoCodeResource.php @@ -16,7 +16,7 @@ public function toArray(Request $request): array return [ 'id' => $this->getId(), 'code' => $this->getCode(), - 'applicable_ticket_ids' => $this->getApplicableTicketIds(), + 'applicable_product_ids' => $this->getApplicableProductIds(), 'discount' => $this->getDiscount(), 'discount_type' => $this->getDiscountType(), 'created_at' => $this->getCreatedAt(), diff --git a/backend/app/Resources/Question/QuestionAnswerViewResource.php b/backend/app/Resources/Question/QuestionAnswerViewResource.php index 6b58307d..7c3fae4f 100644 --- a/backend/app/Resources/Question/QuestionAnswerViewResource.php +++ b/backend/app/Resources/Question/QuestionAnswerViewResource.php @@ -16,6 +16,8 @@ class QuestionAnswerViewResource extends JsonResource public function toArray(Request $request): array { return [ + 'product_id' => $this->getProductId(), + 'product_title' => $this->getProductTitle(), 'question_id' => $this->getQuestionId(), 'title' => $this->getTitle(), 'answer' => $this->getAnswer(), diff --git a/backend/app/Resources/Question/QuestionResource.php b/backend/app/Resources/Question/QuestionResource.php index 7f0591dd..96bdf296 100644 --- a/backend/app/Resources/Question/QuestionResource.php +++ b/backend/app/Resources/Question/QuestionResource.php @@ -23,9 +23,9 @@ public function toArray(Request $request): array 'event_id' => $this->getEventId(), 'belongs_to' => $this->getBelongsTo(), 'is_hidden' => $this->getIsHidden(), - 'ticket_ids' => $this->when( - !is_null($this->getTickets()), - fn() => $this->getTickets()->map(fn($ticket) => $ticket->getId()) + 'product_ids' => $this->when( + !is_null($this->getProducts()), + fn() => $this->getProducts()->map(fn($product) => $product->getId()) ), ]; } diff --git a/backend/app/Resources/Question/QuestionResourcePublic.php b/backend/app/Resources/Question/QuestionResourcePublic.php index 5820cb8b..d4e1288d 100644 --- a/backend/app/Resources/Question/QuestionResourcePublic.php +++ b/backend/app/Resources/Question/QuestionResourcePublic.php @@ -22,9 +22,9 @@ public function toArray(Request $request): array 'required' => $this->getRequired(), 'event_id' => $this->getEventId(), 'belongs_to' => $this->getBelongsTo(), - 'ticket_ids' => $this->when( - !is_null($this->getTickets()), - fn() => $this->getTickets()->map(fn($ticket) => $ticket->getId()) + 'product_ids' => $this->when( + !is_null($this->getProducts()), + fn() => $this->getProducts()->map(fn($product) => $product->getId()) ), ]; } diff --git a/backend/app/Services/Handlers/Account/CreateAccountHandler.php b/backend/app/Services/Application/Handlers/Account/CreateAccountHandler.php similarity index 95% rename from backend/app/Services/Handlers/Account/CreateAccountHandler.php rename to backend/app/Services/Application/Handlers/Account/CreateAccountHandler.php index 2e7c50cf..579be43c 100644 --- a/backend/app/Services/Handlers/Account/CreateAccountHandler.php +++ b/backend/app/Services/Application/Handlers/Account/CreateAccountHandler.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace HiEvents\Services\Handlers\Account; +namespace HiEvents\Services\Application\Handlers\Account; use HiEvents\DomainObjects\AccountDomainObject; use HiEvents\DomainObjects\Enums\Role; @@ -13,10 +13,10 @@ use HiEvents\Repository\Interfaces\AccountRepositoryInterface; use HiEvents\Repository\Interfaces\AccountUserRepositoryInterface; use HiEvents\Repository\Interfaces\UserRepositoryInterface; +use HiEvents\Services\Application\Handlers\Account\DTO\CreateAccountDTO; +use HiEvents\Services\Application\Handlers\Account\Exceptions\AccountRegistrationDisabledException; use HiEvents\Services\Domain\Account\AccountUserAssociationService; use HiEvents\Services\Domain\User\EmailConfirmationService; -use HiEvents\Services\Handlers\Account\DTO\CreateAccountDTO; -use HiEvents\Services\Handlers\Account\Exceptions\AccountRegistrationDisabledException; use Illuminate\Config\Repository; use Illuminate\Database\DatabaseManager; use Illuminate\Hashing\HashManager; diff --git a/backend/app/Services/Handlers/Account/DTO/CreateAccountDTO.php b/backend/app/Services/Application/Handlers/Account/DTO/CreateAccountDTO.php similarity index 88% rename from backend/app/Services/Handlers/Account/DTO/CreateAccountDTO.php rename to backend/app/Services/Application/Handlers/Account/DTO/CreateAccountDTO.php index 94322a2c..0e71e8fb 100644 --- a/backend/app/Services/Handlers/Account/DTO/CreateAccountDTO.php +++ b/backend/app/Services/Application/Handlers/Account/DTO/CreateAccountDTO.php @@ -1,6 +1,6 @@ createOrder($attendeeDTO->event_id, $attendeeDTO); - /** @var TicketDomainObject $ticket */ - $ticket = $this->ticketRepository - ->loadRelation(TicketPriceDomainObject::class) + /** @var ProductDomainObject $product */ + $product = $this->productRepository + ->loadRelation(ProductPriceDomainObject::class) ->findFirstWhere([ - TicketDomainObjectAbstract::ID => $attendeeDTO->ticket_id, - TicketDomainObjectAbstract::EVENT_ID => $attendeeDTO->event_id, + ProductDomainObjectAbstract::ID => $attendeeDTO->product_id, + ProductDomainObjectAbstract::EVENT_ID => $attendeeDTO->event_id, + ProductDomainObjectAbstract::PRODUCT_TYPE => ProductType::TICKET->name, ]); - $ticketPriceId = $this->getTicketPriceId($attendeeDTO, $ticket); + if (!$product) { + throw new NoTicketsAvailableException(__('This ticket is invalid')); + } + + $productPriceId = $this->getProductPriceId($attendeeDTO, $product); - $availableQuantity = $this->ticketRepository->getQuantityRemainingForTicketPrice( - $attendeeDTO->ticket_id, - $ticketPriceId, + $availableQuantity = $this->productRepository->getQuantityRemainingForProductPrice( + $attendeeDTO->product_id, + $productPriceId, ); if ($availableQuantity <= 0) { throw new NoTicketsAvailableException(__('There are no tickets available. ' . - 'If you would like to assign a ticket to this attendee,' . - ' please adjust the ticket\'s available quantity.')); + 'If you would like to assign a product to this attendee,' . + ' please adjust the product\'s available quantity.')); } + $productPriceId = $this->getProductPriceId($attendeeDTO, $product); $this->processTaxesAndFees($attendeeDTO); - $orderItem = $this->createOrderItem($attendeeDTO, $order, $ticket, $ticketPriceId); + $orderItem = $this->createOrderItem($attendeeDTO, $order, $product, $productPriceId); $attendee = $this->createAttendee($order, $attendeeDTO); @@ -123,27 +130,27 @@ private function createOrder(int $eventId, CreateAttendeeDTO $attendeeDTO): Orde } /** - * @throws InvalidTicketPriceId + * @throws InvalidProductPriceId */ - private function getTicketPriceId(CreateAttendeeDTO $attendeeDTO, TicketDomainObject $ticket): int + private function getProductPriceId(CreateAttendeeDTO $attendeeDTO, ProductDomainObject $product): int { - $priceIds = $ticket->getTicketPrices()->map(fn(TicketPriceDomainObject $ticketPrice) => $ticketPrice->getId()); + $priceIds = $product->getProductPrices()->map(fn(ProductPriceDomainObject $productPrice) => $productPrice->getId()); - if ($attendeeDTO->ticket_price_id) { - if (!$priceIds->contains($attendeeDTO->ticket_price_id)) { - throw new InvalidTicketPriceId(__('The ticket price ID is invalid.')); + if ($attendeeDTO->product_price_id) { + if (!$priceIds->contains($attendeeDTO->product_price_id)) { + throw new InvalidProductPriceId(__('The product price ID is invalid.')); } - return $attendeeDTO->ticket_price_id; + return $attendeeDTO->product_price_id; } - /** @var TicketPriceDomainObject $ticketPrice */ - $ticketPrice = $ticket->getTicketPrices()->first(); + /** @var ProductPriceDomainObject $productPrice */ + $productPrice = $product->getProductPrices()->first(); - if ($ticketPrice) { - return $ticketPrice->getId(); + if ($productPrice) { + return $productPrice->getId(); } - throw new InvalidTicketPriceId(__('The ticket price ID is invalid.')); + throw new InvalidProductPriceId(__('The product price ID is invalid.')); } private function calculateTaxesAndFees(CreateAttendeeDTO $attendeeDTO): ?Collection @@ -187,11 +194,11 @@ private function processTaxesAndFees(CreateAttendeeDTO $attendeeDTO): void ); } - private function createOrderItem(CreateAttendeeDTO $attendeeDTO, OrderDomainObject $order, TicketDomainObject $ticket, int $ticketPriceId): OrderItemDomainObject + private function createOrderItem(CreateAttendeeDTO $attendeeDTO, OrderDomainObject $order, ProductDomainObject $product, int $productPriceId): OrderItemDomainObject { return $this->orderRepository->addOrderItem( [ - OrderItemDomainObjectAbstract::TICKET_ID => $attendeeDTO->ticket_id, + OrderItemDomainObjectAbstract::PRODUCT_ID => $attendeeDTO->product_id, OrderItemDomainObjectAbstract::QUANTITY => 1, OrderItemDomainObjectAbstract::TOTAL_BEFORE_ADDITIONS => $attendeeDTO->amount_paid, OrderItemDomainObjectAbstract::TOTAL_GROSS => $attendeeDTO->amount_paid + $this->taxAndFeeRollupService->getTotalTaxesAndFees(), @@ -199,8 +206,8 @@ private function createOrderItem(CreateAttendeeDTO $attendeeDTO, OrderDomainObje OrderItemDomainObjectAbstract::TOTAL_SERVICE_FEE => $this->taxAndFeeRollupService->getTotalFees(), OrderItemDomainObjectAbstract::PRICE => $attendeeDTO->amount_paid, OrderItemDomainObjectAbstract::ORDER_ID => $order->getId(), - OrderItemDomainObjectAbstract::ITEM_NAME => $ticket->getTitle(), - OrderItemDomainObjectAbstract::TICKET_PRICE_ID => $ticketPriceId, + OrderItemDomainObjectAbstract::ITEM_NAME => $product->getTitle(), + OrderItemDomainObjectAbstract::PRODUCT_PRICE_ID => $productPriceId, OrderItemDomainObjectAbstract::TAXES_AND_FEES_ROLLUP => $this->taxAndFeeRollupService->getRollUp(), ] ); @@ -210,8 +217,8 @@ private function createAttendee(OrderDomainObject $order, CreateAttendeeDTO $att { return $this->attendeeRepository->create([ AttendeeDomainObjectAbstract::EVENT_ID => $order->getEventId(), - AttendeeDomainObjectAbstract::TICKET_ID => $attendeeDTO->ticket_id, - AttendeeDomainObjectAbstract::TICKET_PRICE_ID => $attendeeDTO->ticket_price_id, + AttendeeDomainObjectAbstract::PRODUCT_ID => $attendeeDTO->product_id, + AttendeeDomainObjectAbstract::PRODUCT_PRICE_ID => $attendeeDTO->product_price_id, AttendeeDomainObjectAbstract::STATUS => AttendeeStatus::ACTIVE->name, AttendeeDomainObjectAbstract::EMAIL => $attendeeDTO->email, AttendeeDomainObjectAbstract::FIRST_NAME => $attendeeDTO->first_name, @@ -225,8 +232,8 @@ private function createAttendee(OrderDomainObject $order, CreateAttendeeDTO $att private function fireEventsAndUpdateQuantities(CreateAttendeeDTO $attendeeDTO, OrderDomainObject $order): void { - $this->ticketQuantityAdjustmentService->increaseQuantitySold( - priceId: $attendeeDTO->ticket_price_id, + $this->productQuantityAdjustmentService->increaseQuantitySold( + priceId: $attendeeDTO->product_price_id, ); event(new OrderStatusChangedEvent( diff --git a/backend/app/Services/Handlers/Attendee/DTO/CheckInAttendeeDTO.php b/backend/app/Services/Application/Handlers/Attendee/DTO/CheckInAttendeeDTO.php similarity index 82% rename from backend/app/Services/Handlers/Attendee/DTO/CheckInAttendeeDTO.php rename to backend/app/Services/Application/Handlers/Attendee/DTO/CheckInAttendeeDTO.php index 6a1b0810..60c75ae9 100644 --- a/backend/app/Services/Handlers/Attendee/DTO/CheckInAttendeeDTO.php +++ b/backend/app/Services/Application/Handlers/Attendee/DTO/CheckInAttendeeDTO.php @@ -1,6 +1,6 @@ databaseManager->transaction(function () use ($editAttendeeDTO) { + $this->validateProductId($editAttendeeDTO); + + $attendee = $this->getAttendee($editAttendeeDTO); + + $this->adjustProductQuantities($attendee, $editAttendeeDTO); + + return $this->updateAttendee($editAttendeeDTO); + }); + } + + private function adjustProductQuantities(AttendeeDomainObject $attendee, EditAttendeeDTO $editAttendeeDTO): void + { + if ($attendee->getProductPriceId() !== $editAttendeeDTO->product_price_id) { + $this->productQuantityService->decreaseQuantitySold($attendee->getProductPriceId()); + $this->productQuantityService->increaseQuantitySold($editAttendeeDTO->product_price_id); + } + } + + private function updateAttendee(EditAttendeeDTO $editAttendeeDTO): AttendeeDomainObject + { + return $this->attendeeRepository->updateByIdWhere($editAttendeeDTO->attendee_id, [ + 'first_name' => $editAttendeeDTO->first_name, + 'last_name' => $editAttendeeDTO->last_name, + 'email' => $editAttendeeDTO->email, + 'product_id' => $editAttendeeDTO->product_id, + 'product_price_id' => $editAttendeeDTO->product_price_id, + 'notes' => $editAttendeeDTO->notes, + ], [ + 'event_id' => $editAttendeeDTO->event_id, + ]); + } + + /** + * @throws ValidationException + * @throws NoTicketsAvailableException + */ + private function validateProductId(EditAttendeeDTO $editAttendeeDTO): void + { + /** @var ProductDomainObject $product */ + $product = $this->productRepository + ->loadRelation(ProductPriceDomainObject::class) + ->findFirstWhere([ + ProductDomainObjectAbstract::ID => $editAttendeeDTO->product_id, + ]); + + if ($product->getEventId() !== $editAttendeeDTO->event_id) { + throw ValidationException::withMessages([ + 'product_id' => __('Product ID is not valid'), + ]); + } + + $productPriceIds = $product->getProductPrices()->map(fn($productPrice) => $productPrice->getId())->toArray(); + if (!in_array($editAttendeeDTO->product_price_id, $productPriceIds, true)) { + throw ValidationException::withMessages([ + 'product_price_id' => __('Product price ID is not valid'), + ]); + } + + $availableQuantity = $this->productRepository->getQuantityRemainingForProductPrice( + productId: $editAttendeeDTO->product_id, + productPriceId: $product->getType() === ProductPriceType::TIERED->name + ? $editAttendeeDTO->product_price_id + : $product->getProductPrices()->first()->getId(), + ); + + if ($availableQuantity <= 0) { + throw new NoTicketsAvailableException( + __('There are no products available. If you would like to assign this product to this attendee, please adjust the product\'s available quantity.') + ); + } + } + + /** + * @throws ValidationException + */ + private function getAttendee(EditAttendeeDTO $editAttendeeDTO): AttendeeDomainObject + { + $attendee = $this->attendeeRepository->findFirstWhere([ + AttendeeDomainObjectAbstract::EVENT_ID => $editAttendeeDTO->event_id, + AttendeeDomainObjectAbstract::ID => $editAttendeeDTO->attendee_id, + ]); + + if ($attendee === null) { + throw ValidationException::withMessages([ + 'attendee_id' => __('Attendee ID is not valid'), + ]); + } + + return $attendee; + } +} diff --git a/backend/app/Services/Handlers/Attendee/PartialEditAttendeeHandler.php b/backend/app/Services/Application/Handlers/Attendee/PartialEditAttendeeHandler.php similarity index 67% rename from backend/app/Services/Handlers/Attendee/PartialEditAttendeeHandler.php rename to backend/app/Services/Application/Handlers/Attendee/PartialEditAttendeeHandler.php index 5717bddf..20ccd053 100644 --- a/backend/app/Services/Handlers/Attendee/PartialEditAttendeeHandler.php +++ b/backend/app/Services/Application/Handlers/Attendee/PartialEditAttendeeHandler.php @@ -1,12 +1,12 @@ status && $data->status !== $attendee->getStatus()) { - $this->adjustTicketQuantity($data, $attendee); + $this->adjustProductQuantity($data, $attendee); } return $this->attendeeRepository->updateByIdWhere( @@ -62,14 +62,14 @@ private function updateAttendee(PartialEditAttendeeDTO $data): AttendeeDomainObj } /** - * @todo - we should check ticket availability before updating the ticket quantity + * @todo - we should check product availability before updating the product quantity */ - private function adjustTicketQuantity(PartialEditAttendeeDTO $data, AttendeeDomainObject $attendee): void + private function adjustProductQuantity(PartialEditAttendeeDTO $data, AttendeeDomainObject $attendee): void { if ($data->status === AttendeeStatus::ACTIVE->name) { - $this->ticketQuantityService->increaseQuantitySold($attendee->getTicketPriceId()); + $this->productQuantityService->increaseQuantitySold($attendee->getProductPriceId()); } elseif ($data->status === AttendeeStatus::CANCELLED->name) { - $this->ticketQuantityService->decreaseQuantitySold($attendee->getTicketPriceId()); + $this->productQuantityService->decreaseQuantitySold($attendee->getProductPriceId()); } } } diff --git a/backend/app/Services/Handlers/Attendee/ResendAttendeeTicketHandler.php b/backend/app/Services/Application/Handlers/Attendee/ResendAttendeeTicketHandler.php similarity index 74% rename from backend/app/Services/Handlers/Attendee/ResendAttendeeTicketHandler.php rename to backend/app/Services/Application/Handlers/Attendee/ResendAttendeeTicketHandler.php index 1424a162..7f1afaba 100644 --- a/backend/app/Services/Handlers/Attendee/ResendAttendeeTicketHandler.php +++ b/backend/app/Services/Application/Handlers/Attendee/ResendAttendeeTicketHandler.php @@ -1,6 +1,6 @@ attendeeRepository->findFirstWhere([ - 'id' => $resendAttendeeTicketDTO->attendeeId, - 'event_id' => $resendAttendeeTicketDTO->eventId, + 'id' => $resendAttendeeProductDTO->attendeeId, + 'event_id' => $resendAttendeeProductDTO->eventId, ]); if (!$attendee) { @@ -46,9 +46,9 @@ public function handle(ResendAttendeeTicketDTO $resendAttendeeTicketDTO): void $event = $this->eventRepository ->loadRelation(new Relationship(OrganizerDomainObject::class, name: 'organizer')) ->loadRelation(EventSettingDomainObject::class) - ->findById($resendAttendeeTicketDTO->eventId); + ->findById($resendAttendeeProductDTO->eventId); - $this->sendAttendeeTicketService->send( + $this->sendAttendeeProductService->send( attendee: $attendee, event: $event, eventSettings: $event->getEventSettings(), @@ -56,8 +56,8 @@ public function handle(ResendAttendeeTicketDTO $resendAttendeeTicketDTO): void ); $this->logger->info('Attendee ticket resent', [ - 'attendeeId' => $resendAttendeeTicketDTO->attendeeId, - 'eventId' => $resendAttendeeTicketDTO->eventId + 'attendeeId' => $resendAttendeeProductDTO->attendeeId, + 'eventId' => $resendAttendeeProductDTO->eventId ]); } } diff --git a/backend/app/Services/Handlers/Auth/AcceptInvitationHandler.php b/backend/app/Services/Application/Handlers/Auth/AcceptInvitationHandler.php similarity index 95% rename from backend/app/Services/Handlers/Auth/AcceptInvitationHandler.php rename to backend/app/Services/Application/Handlers/Auth/AcceptInvitationHandler.php index b1709bbc..fd46007e 100644 --- a/backend/app/Services/Handlers/Auth/AcceptInvitationHandler.php +++ b/backend/app/Services/Application/Handlers/Auth/AcceptInvitationHandler.php @@ -1,12 +1,12 @@ setName($data->name) ->setEventId($data->event_id) ->setCapacity($data->capacity) - ->setAppliesTo(CapacityAssignmentAppliesTo::TICKETS->name) + ->setAppliesTo(CapacityAssignmentAppliesTo::PRODUCTS->name) ->setStatus($data->status->name); return $this->createCapacityAssignmentService->createCapacityAssignment( $capacityAssignment, - $data->ticket_ids, + $data->product_ids, ); } } diff --git a/backend/app/Services/Handlers/CapacityAssignment/DTO/GetCapacityAssignmentsDTO.php b/backend/app/Services/Application/Handlers/CapacityAssignment/DTO/GetCapacityAssignmentsDTO.php similarity index 79% rename from backend/app/Services/Handlers/CapacityAssignment/DTO/GetCapacityAssignmentsDTO.php rename to backend/app/Services/Application/Handlers/CapacityAssignment/DTO/GetCapacityAssignmentsDTO.php index 7cc7da5e..a7d8752f 100644 --- a/backend/app/Services/Handlers/CapacityAssignment/DTO/GetCapacityAssignmentsDTO.php +++ b/backend/app/Services/Application/Handlers/CapacityAssignment/DTO/GetCapacityAssignmentsDTO.php @@ -1,6 +1,6 @@ databaseManager->transaction(function () use ($id, $eventId) { - $this->ticketRepository->removeCapacityAssignmentFromTickets( + $this->productRepository->removeCapacityAssignmentFromProducts( capacityAssignmentId: $id, ); diff --git a/backend/app/Services/Handlers/CapacityAssignment/GetCapacityAssignmentHandler.php b/backend/app/Services/Application/Handlers/CapacityAssignment/GetCapacityAssignmentHandler.php similarity index 83% rename from backend/app/Services/Handlers/CapacityAssignment/GetCapacityAssignmentHandler.php rename to backend/app/Services/Application/Handlers/CapacityAssignment/GetCapacityAssignmentHandler.php index cbdf770f..a8340b6e 100644 --- a/backend/app/Services/Handlers/CapacityAssignment/GetCapacityAssignmentHandler.php +++ b/backend/app/Services/Application/Handlers/CapacityAssignment/GetCapacityAssignmentHandler.php @@ -1,9 +1,9 @@ capacityAssignmentRepository - ->loadRelation(TicketDomainObject::class) + ->loadRelation(ProductDomainObject::class) ->findFirstWhere([ 'event_id' => $eventId, 'id' => $capacityAssignmentId, diff --git a/backend/app/Services/Handlers/CapacityAssignment/GetCapacityAssignmentsHandler.php b/backend/app/Services/Application/Handlers/CapacityAssignment/GetCapacityAssignmentsHandler.php similarity index 69% rename from backend/app/Services/Handlers/CapacityAssignment/GetCapacityAssignmentsHandler.php rename to backend/app/Services/Application/Handlers/CapacityAssignment/GetCapacityAssignmentsHandler.php index e4d0434b..215ffe56 100644 --- a/backend/app/Services/Handlers/CapacityAssignment/GetCapacityAssignmentsHandler.php +++ b/backend/app/Services/Application/Handlers/CapacityAssignment/GetCapacityAssignmentsHandler.php @@ -1,10 +1,10 @@ capacityAssignmentRepository - ->loadRelation(TicketDomainObject::class) + ->loadRelation(ProductDomainObject::class) ->findByEventId( eventId: $dto->eventId, params: $dto->queryParams, diff --git a/backend/app/Services/Handlers/CapacityAssignment/UpdateCapacityAssignmentHandler.php b/backend/app/Services/Application/Handlers/CapacityAssignment/UpdateCapacityAssignmentHandler.php similarity index 69% rename from backend/app/Services/Handlers/CapacityAssignment/UpdateCapacityAssignmentHandler.php rename to backend/app/Services/Application/Handlers/CapacityAssignment/UpdateCapacityAssignmentHandler.php index 9c330d85..eedf3eef 100644 --- a/backend/app/Services/Handlers/CapacityAssignment/UpdateCapacityAssignmentHandler.php +++ b/backend/app/Services/Application/Handlers/CapacityAssignment/UpdateCapacityAssignmentHandler.php @@ -1,12 +1,12 @@ setName($data->name) ->setEventId($data->event_id) ->setCapacity($data->capacity) - ->setAppliesTo(CapacityAssignmentAppliesTo::TICKETS->name) + ->setAppliesTo(CapacityAssignmentAppliesTo::PRODUCTS->name) ->setStatus($data->status->name); return $this->updateCapacityAssignmentService->updateCapacityAssignment( $capacityAssignment, - $data->ticket_ids, + $data->product_ids, ); } } diff --git a/backend/app/Services/Handlers/CheckInList/CreateCheckInListHandler.php b/backend/app/Services/Application/Handlers/CheckInList/CreateCheckInListHandler.php similarity index 72% rename from backend/app/Services/Handlers/CheckInList/CreateCheckInListHandler.php rename to backend/app/Services/Application/Handlers/CheckInList/CreateCheckInListHandler.php index a3d5ed45..6682b2a3 100644 --- a/backend/app/Services/Handlers/CheckInList/CreateCheckInListHandler.php +++ b/backend/app/Services/Application/Handlers/CheckInList/CreateCheckInListHandler.php @@ -1,11 +1,11 @@ createCheckInListService->createCheckInList( checkInList: $checkInList, - ticketIds: $listData->ticketIds + productIds: $listData->productIds ); } } diff --git a/backend/app/Services/Handlers/CheckInList/DTO/GetCheckInListsDTO.php b/backend/app/Services/Application/Handlers/CheckInList/DTO/GetCheckInListsDTO.php similarity index 80% rename from backend/app/Services/Handlers/CheckInList/DTO/GetCheckInListsDTO.php rename to backend/app/Services/Application/Handlers/CheckInList/DTO/GetCheckInListsDTO.php index ebc04d3d..68158597 100644 --- a/backend/app/Services/Handlers/CheckInList/DTO/GetCheckInListsDTO.php +++ b/backend/app/Services/Application/Handlers/CheckInList/DTO/GetCheckInListsDTO.php @@ -1,6 +1,6 @@ checkInListRepository - ->loadRelation(TicketDomainObject::class) + ->loadRelation(ProductDomainObject::class) ->loadRelation(new Relationship(domainObject: EventDomainObject::class, name: 'event')) ->findFirstWhere([ 'event_id' => $eventId, diff --git a/backend/app/Services/Handlers/CheckInList/GetCheckInListsHandler.php b/backend/app/Services/Application/Handlers/CheckInList/GetCheckInListsHandler.php similarity index 87% rename from backend/app/Services/Handlers/CheckInList/GetCheckInListsHandler.php rename to backend/app/Services/Application/Handlers/CheckInList/GetCheckInListsHandler.php index be46595b..3725d05f 100644 --- a/backend/app/Services/Handlers/CheckInList/GetCheckInListsHandler.php +++ b/backend/app/Services/Application/Handlers/CheckInList/GetCheckInListsHandler.php @@ -1,13 +1,13 @@ checkInListRepository - ->loadRelation(TicketDomainObject::class) + ->loadRelation(ProductDomainObject::class) ->loadRelation(new Relationship(domainObject: EventDomainObject::class, name: 'event')) ->findByEventId( eventId: $dto->eventId, diff --git a/backend/app/Services/Handlers/CheckInList/Public/CreateAttendeeCheckInPublicHandler.php b/backend/app/Services/Application/Handlers/CheckInList/Public/CreateAttendeeCheckInPublicHandler.php similarity index 88% rename from backend/app/Services/Handlers/CheckInList/Public/CreateAttendeeCheckInPublicHandler.php rename to backend/app/Services/Application/Handlers/CheckInList/Public/CreateAttendeeCheckInPublicHandler.php index de22ea2f..79283e41 100644 --- a/backend/app/Services/Handlers/CheckInList/Public/CreateAttendeeCheckInPublicHandler.php +++ b/backend/app/Services/Application/Handlers/CheckInList/Public/CreateAttendeeCheckInPublicHandler.php @@ -1,12 +1,12 @@ checkInListRepository - ->loadRelation(TicketDomainObject::class) + ->loadRelation(ProductDomainObject::class) ->loadRelation(new Relationship(EventDomainObject::class, name: 'event')) ->findFirstWhere([ CheckInListDomainObjectAbstract::SHORT_ID => $shortId, diff --git a/backend/app/Services/Handlers/CheckInList/Public/GetCheckInListPublicHandler.php b/backend/app/Services/Application/Handlers/CheckInList/Public/GetCheckInListPublicHandler.php similarity index 87% rename from backend/app/Services/Handlers/CheckInList/Public/GetCheckInListPublicHandler.php rename to backend/app/Services/Application/Handlers/CheckInList/Public/GetCheckInListPublicHandler.php index bbd24480..703db867 100644 --- a/backend/app/Services/Handlers/CheckInList/Public/GetCheckInListPublicHandler.php +++ b/backend/app/Services/Application/Handlers/CheckInList/Public/GetCheckInListPublicHandler.php @@ -1,10 +1,10 @@ checkInListRepository ->loadRelation(new Relationship(domainObject: EventDomainObject::class, name: 'event')) - ->loadRelation(TicketDomainObject::class) + ->loadRelation(ProductDomainObject::class) ->findFirstWhere([ 'short_id' => $shortId, ]); diff --git a/backend/app/Services/Handlers/CheckInList/UpdateCheckInlistHandler.php b/backend/app/Services/Application/Handlers/CheckInList/UpdateCheckInlistHandler.php similarity index 72% rename from backend/app/Services/Handlers/CheckInList/UpdateCheckInlistHandler.php rename to backend/app/Services/Application/Handlers/CheckInList/UpdateCheckInlistHandler.php index 44307817..d31d7873 100644 --- a/backend/app/Services/Handlers/CheckInList/UpdateCheckInlistHandler.php +++ b/backend/app/Services/Application/Handlers/CheckInList/UpdateCheckInlistHandler.php @@ -1,11 +1,11 @@ updateCheckInlistService->updateCheckInlist( checkInList: $checkInList, - ticketIds: $data->ticketIds + productIds: $data->productIds ); } } diff --git a/backend/app/Services/Handlers/Event/CreateEventHandler.php b/backend/app/Services/Application/Handlers/Event/CreateEventHandler.php similarity index 93% rename from backend/app/Services/Handlers/Event/CreateEventHandler.php rename to backend/app/Services/Application/Handlers/Event/CreateEventHandler.php index ae6e7b3f..652fad18 100644 --- a/backend/app/Services/Handlers/Event/CreateEventHandler.php +++ b/backend/app/Services/Application/Handlers/Event/CreateEventHandler.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace HiEvents\Services\Handlers\Event; +namespace HiEvents\Services\Application\Handlers\Event; use HiEvents\DomainObjects\EventDomainObject; use HiEvents\Exceptions\OrganizerNotFoundException; +use HiEvents\Services\Application\Handlers\Event\DTO\CreateEventDTO; use HiEvents\Services\Domain\Event\CreateEventService; use HiEvents\Services\Domain\Organizer\OrganizerFetchService; -use HiEvents\Services\Handlers\Event\DTO\CreateEventDTO; use Throwable; class CreateEventHandler diff --git a/backend/app/Services/Handlers/Event/CreateEventImageHandler.php b/backend/app/Services/Application/Handlers/Event/CreateEventImageHandler.php similarity index 82% rename from backend/app/Services/Handlers/Event/CreateEventImageHandler.php rename to backend/app/Services/Application/Handlers/Event/CreateEventImageHandler.php index 483548b8..fbfd8325 100644 --- a/backend/app/Services/Handlers/Event/CreateEventImageHandler.php +++ b/backend/app/Services/Application/Handlers/Event/CreateEventImageHandler.php @@ -1,10 +1,10 @@ accountId, title: $data->title, startDate: $data->startDate, - duplicateTickets: $data->duplicateTickets, + duplicateProducts: $data->duplicateProducts, duplicateQuestions: $data->duplicateQuestions, duplicateSettings: $data->duplicateSettings, duplicatePromoCodes: $data->duplicatePromoCodes, diff --git a/backend/app/Services/Handlers/Event/GetEventCheckInStatsHandler.php b/backend/app/Services/Application/Handlers/Event/GetEventCheckInStatsHandler.php similarity index 88% rename from backend/app/Services/Handlers/Event/GetEventCheckInStatsHandler.php rename to backend/app/Services/Application/Handlers/Event/GetEventCheckInStatsHandler.php index 388f6350..f47aa228 100644 --- a/backend/app/Services/Handlers/Event/GetEventCheckInStatsHandler.php +++ b/backend/app/Services/Application/Handlers/Event/GetEventCheckInStatsHandler.php @@ -1,6 +1,6 @@ eventRepository ->loadRelation( - new Relationship(TicketDomainObject::class, [ - new Relationship(TicketPriceDomainObject::class), - new Relationship(TaxAndFeesDomainObject::class) + new Relationship(ProductCategoryDomainObject::class, [ + new Relationship(ProductDomainObject::class, [ + new Relationship(ProductPriceDomainObject::class), + new Relationship(TaxAndFeesDomainObject::class), + ]), ]) ) ->loadRelation(new Relationship(EventSettingDomainObject::class)) @@ -55,6 +58,9 @@ public function handle(GetPublicEventDTO $data): EventDomainObject $this->eventPageViewIncrementService->increment($data->eventId, $data->ipAddress); } - return $event->setTickets($this->ticketFilterService->filter($event->getTickets(), $promoCodeDomainObject)); + return $event->setProductCategories($this->productFilterService->filter( + productsCategories: $event->getProductCategories(), + promoCode: $promoCodeDomainObject + )); } } diff --git a/backend/app/Services/Handlers/Event/UpdateEventHandler.php b/backend/app/Services/Application/Handlers/Event/UpdateEventHandler.php similarity index 96% rename from backend/app/Services/Handlers/Event/UpdateEventHandler.php rename to backend/app/Services/Application/Handlers/Event/UpdateEventHandler.php index f872725a..6a57c299 100644 --- a/backend/app/Services/Handlers/Event/UpdateEventHandler.php +++ b/backend/app/Services/Application/Handlers/Event/UpdateEventHandler.php @@ -1,6 +1,6 @@ $messageData->type->name, 'order_id' => $this->getOrderId($messageData), 'attendee_ids' => $this->getAttendeeIds($messageData)->toArray(), - 'ticket_ids' => $this->getTicketIds($messageData)->toArray(), + 'product_ids' => $this->getProductIds($messageData)->toArray(), 'sent_at' => Carbon::now()->toDateTimeString(), 'sent_by_user_id' => $messageData->sent_by_user_id, 'status' => MessageStatus::PROCESSING->name, @@ -63,7 +63,7 @@ public function handle(SendMessageDTO $messageData): MessageDomainObject 'is_test' => $messageData->is_test, 'order_id' => $message->getOrderId(), 'attendee_ids' => $message->getAttendeeIds(), - 'ticket_ids' => $message->getTicketIds(), + 'product_ids' => $message->getProductIds(), 'send_copy_to_current_user' => $messageData->send_copy_to_current_user, 'sent_by_user_id' => $messageData->sent_by_user_id, 'account_id' => $messageData->account_id, @@ -90,18 +90,18 @@ private function getAttendeeIds(SendMessageDTO $messageData): Collection } - private function getTicketIds(SendMessageDTO $messageData): Collection + private function getProductIds(SendMessageDTO $messageData): Collection { - $tickets = $this->ticketRepository->findWhereIn( + $products = $this->productRepository->findWhereIn( field: 'id', - values: $messageData->ticket_ids, + values: $messageData->product_ids, additionalWhere: [ 'event_id' => $messageData->event_id, ], columns: ['id'] ); - return $tickets->map(fn($attendee) => $attendee->getId()); + return $products->map(fn($attendee) => $attendee->getId()); } private function getOrderId(SendMessageDTO $messageData): ?int diff --git a/backend/app/Services/Handlers/Order/CancelOrderHandler.php b/backend/app/Services/Application/Handlers/Order/CancelOrderHandler.php similarity index 91% rename from backend/app/Services/Handlers/Order/CancelOrderHandler.php rename to backend/app/Services/Application/Handlers/Order/CancelOrderHandler.php index dec7171e..b385ee8c 100644 --- a/backend/app/Services/Handlers/Order/CancelOrderHandler.php +++ b/backend/app/Services/Application/Handlers/Order/CancelOrderHandler.php @@ -1,13 +1,13 @@ updateOrder($order, $orderDTO); - $this->createAttendees($orderData->attendees, $order); + $this->createAttendees($orderData->products, $order); if ($orderData->order->questions) { $this->createOrderQuestions($orderDTO->questions, $order); } /** - * If there's no payment required, immediately update the ticket quantities, otherwise handle + * If there's no payment required, immediately update the product quantities, otherwise handle * this in the PaymentIntentEventHandlerService * * @see PaymentIntentSucceededHandler */ if (!$order->isPaymentRequired()) { - $this->ticketQuantityUpdateService->updateQuantitiesFromOrder($updatedOrder); + $this->productQuantityUpdateService->updateQuantitiesFromOrder($updatedOrder); } OrderStatusChangedEvent::dispatch($updatedOrder); @@ -86,49 +88,69 @@ public function handle(string $orderShortId, CompleteOrderDTO $orderData): Order } /** + * @param Collection $orderProducts * @throws Exception */ - private function createAttendees(Collection $attendees, OrderDomainObject $order): void + private function createAttendees(Collection $orderProducts, OrderDomainObject $order): void { $inserts = []; + $createdProductData = collect(); - $ticketsPrices = $this->ticketPriceRepository->findWhereIn( - field: TicketPriceDomainObjectAbstract::ID, - values: $attendees->pluck('ticket_price_id')->toArray(), + $productsPrices = $this->productPriceRepository->findWhereIn( + field: ProductPriceDomainObjectAbstract::ID, + values: $orderProducts->pluck('product_price_id')->toArray(), ); - $this->validateTicketPriceIdsMatchOrder($order, $ticketsPrices); - $this->validateAttendees($order, $attendees); + $this->validateProductPriceIdsMatchOrder($order, $productsPrices); + $this->validateTicketProductsCount($order, $orderProducts); - foreach ($attendees as $attendee) { - $ticketId = $ticketsPrices->first( - fn(TicketPriceDomainObject $ticketPrice) => $ticketPrice->getId() === $attendee->ticket_price_id) - ->getTicketId(); + foreach ($orderProducts as $attendee) { + $productId = $productsPrices->first( + fn(ProductPriceDomainObject $productPrice) => $productPrice->getId() === $attendee->product_price_id) + ->getProductId(); + $productType = $this->getProductTypeFromPriceId($attendee->product_price_id, $order->getOrderItems()); + + // If it's not a ticket, skip, as we only want to create attendees for tickets + if ($productType !== ProductType::TICKET->name) { + $createdProductData->push(new CreatedProductDataDTO( + productRequestData: $attendee, + shortId: null, + )); + + continue; + } + + $shortId = IdHelper::shortId(IdHelper::ATTENDEE_PREFIX); $inserts[] = [ AttendeeDomainObjectAbstract::EVENT_ID => $order->getEventId(), - AttendeeDomainObjectAbstract::TICKET_ID => $ticketId, - AttendeeDomainObjectAbstract::TICKET_PRICE_ID => $attendee->ticket_price_id, + AttendeeDomainObjectAbstract::PRODUCT_ID => $productId, + AttendeeDomainObjectAbstract::PRODUCT_PRICE_ID => $attendee->product_price_id, AttendeeDomainObjectAbstract::STATUS => AttendeeStatus::ACTIVE->name, AttendeeDomainObjectAbstract::EMAIL => $attendee->email, AttendeeDomainObjectAbstract::FIRST_NAME => $attendee->first_name, AttendeeDomainObjectAbstract::LAST_NAME => $attendee->last_name, AttendeeDomainObjectAbstract::ORDER_ID => $order->getId(), AttendeeDomainObjectAbstract::PUBLIC_ID => IdHelper::publicId(IdHelper::ATTENDEE_PREFIX), - AttendeeDomainObjectAbstract::SHORT_ID => IdHelper::shortId(IdHelper::ATTENDEE_PREFIX), + AttendeeDomainObjectAbstract::SHORT_ID => $shortId, AttendeeDomainObjectAbstract::LOCALE => $order->getLocale(), ]; + + $createdProductData->push(new CreatedProductDataDTO( + productRequestData: $attendee, + shortId: $shortId, + )); } if (!$this->attendeeRepository->insert($inserts)) { throw new RuntimeException(__('Failed to create attendee')); } - $insertedAttendees = $this->attendeeRepository->findWhere([ - AttendeeDomainObjectAbstract::ORDER_ID => $order->getId() - ]); - - $this->createAttendeeQuestions($attendees, $insertedAttendees, $order, $ticketsPrices); + $this->createProductQuestions( + createdAttendees: $createdProductData, + order: $order, + productPrices: $productsPrices, + ); } private function createOrderQuestions(Collection $questions, OrderDomainObject $order): void @@ -145,32 +167,39 @@ private function createOrderQuestions(Collection $questions, OrderDomainObject $ }); } - private function createAttendeeQuestions( - Collection $attendees, - Collection $insertedAttendees, + /** + * @param Collection $createdAttendees + * @param Collection $productPrices + * @throws ResourceConflictException|Exception + */ + private function createProductQuestions( + Collection $createdAttendees, OrderDomainObject $order, - Collection $ticketPrices, + Collection $productPrices ): void { - $insertedIds = []; - /** @var CompleteOrderAttendeeDTO $attendee */ - foreach ($attendees as $attendee) { - $ticketId = $ticketPrices->first( - fn(TicketPriceDomainObject $ticketPrice) => $ticketPrice->getId() === $attendee->ticket_price_id) - ->getTicketId(); - - $attendeeIterator = $insertedAttendees->filter( - fn(AttendeeDomainObject $insertedAttendee) => $insertedAttendee->getTicketId() === $ticketId - && !in_array($insertedAttendee->getId(), $insertedIds, true) - )->getIterator(); - - if ($attendee->questions === null) { + $newAttendees = $this->attendeeRepository->findWhereIn( + field: AttendeeDomainObjectAbstract::SHORT_ID, + values: $createdAttendees->pluck('shortId')->toArray(), + ); + + foreach ($createdAttendees as $createdAttendee) { + $productRequestData = $createdAttendee->productRequestData; + + if ($productRequestData->questions === null) { continue; } - foreach ($attendee->questions as $question) { - $attendeeId = $attendeeIterator->current()->getId(); + $productId = $productPrices->first( + fn(ProductPriceDomainObject $productPrice) => $productPrice->getId() === $productRequestData->product_price_id + )->getProductId(); + + // This will be null for non-ticket products + $insertedAttendee = $newAttendees->first( + fn(AttendeeDomainObject $attendee) => $attendee->getShortId() === $createdAttendee->shortId, + ); + foreach ($productRequestData->questions as $question) { if (empty($question->response)) { continue; } @@ -179,11 +208,9 @@ private function createAttendeeQuestions( 'question_id' => $question->question_id, 'answer' => $question->response['answer'] ?? $question->response, 'order_id' => $order->getId(), - 'ticket_id' => $ticketId, - 'attendee_id' => $attendeeId + 'product_id' => $productId, + 'attendee_id' => $insertedAttendee?->getId(), ]); - - $insertedIds[] = $attendeeId; } } } @@ -215,7 +242,7 @@ private function getOrder(string $orderShortId): OrderDomainObject ->loadRelation( new Relationship( domainObject: OrderItemDomainObject::class, - nested: [new Relationship(TicketDomainObject::class, name: 'ticket')] + nested: [new Relationship(ProductDomainObject::class, name: 'product')] )) ->findByShortId($orderShortId); @@ -249,33 +276,49 @@ private function updateOrder(OrderDomainObject $order, CompleteOrderOrderDTO $or } /** - * Check if the passed ticket price IDs match what exist in the order_items table + * Check if the passed product price IDs match what exist in the order_items table * * @throws ResourceConflictException */ - private function validateTicketPriceIdsMatchOrder(OrderDomainObject $order, Collection $ticketsPrices): void + private function validateProductPriceIdsMatchOrder(OrderDomainObject $order, Collection $productsPrices): void { - $orderTicketPriceIds = $order->getOrderItems() - ?->map(fn(OrderItemDomainObject $orderItem) => $orderItem->getTicketPriceId())->toArray(); + $orderProductPriceIds = $order->getOrderItems() + ?->map(fn(OrderItemDomainObject $orderItem) => $orderItem->getProductPriceId())->toArray(); - $ticketsPricesIds = $ticketsPrices->map(fn(TicketPriceDomainObject $ticketPrice) => $ticketPrice->getId()); + $productsPricesIds = $productsPrices->map(fn(ProductPriceDomainObject $productPrice) => $productPrice->getId()); - if ($ticketsPricesIds->diff($orderTicketPriceIds)->isNotEmpty()) { - throw new ResourceConflictException(__('There is an unexpected ticket price ID in the order')); + if ($productsPricesIds->diff($orderProductPriceIds)->isNotEmpty()) { + throw new ResourceConflictException(__('There is an unexpected product price ID in the order')); } } /** * @throws ResourceConflictException */ - private function validateAttendees(OrderDomainObject $order, Collection $attendees): void + private function validateTicketProductsCount(OrderDomainObject $order, Collection $attendees): void { - $orderAttendeeCount = $order->getOrderItems()->sum(fn(OrderItemDomainObject $orderItem) => $orderItem->getQuantity()); - - if ($orderAttendeeCount !== $attendees->count()) { + $orderAttendeeCount = $order->getOrderItems() + ?->filter(fn(OrderItemDomainObject $orderItem) => $orderItem->getProductType() === ProductType::TICKET->name) + ?->sum(fn(OrderItemDomainObject $orderItem) => $orderItem->getQuantity()); + + $ticketAttendeeCount = $attendees + ->filter( + fn(CompleteOrderProductDataDTO $attendee) => $this->getProductTypeFromPriceId( + $attendee->product_price_id, + $order->getOrderItems() + ) === ProductType::TICKET->name) + ->count(); + + if ($orderAttendeeCount !== $ticketAttendeeCount) { throw new ResourceConflictException( __('The number of attendees does not match the number of tickets in the order') ); } } + + private function getProductTypeFromPriceId(int $priceId, Collection $orderItems): string + { + return $orderItems->first(fn(OrderItemDomainObject $orderItem) => $orderItem->getProductPriceId() === $priceId) + ->getProductType(); + } } diff --git a/backend/app/Services/Handlers/Order/CreateOrderHandler.php b/backend/app/Services/Application/Handlers/Order/CreateOrderHandler.php similarity index 94% rename from backend/app/Services/Handlers/Order/CreateOrderHandler.php rename to backend/app/Services/Application/Handlers/Order/CreateOrderHandler.php index ba63055e..7f578c46 100644 --- a/backend/app/Services/Handlers/Order/CreateOrderHandler.php +++ b/backend/app/Services/Application/Handlers/Order/CreateOrderHandler.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace HiEvents\Services\Handlers\Order; +namespace HiEvents\Services\Application\Handlers\Order; use HiEvents\DomainObjects\EventDomainObject; use HiEvents\DomainObjects\EventSettingDomainObject; @@ -12,9 +12,9 @@ use HiEvents\DomainObjects\Status\EventStatus; use HiEvents\Repository\Interfaces\EventRepositoryInterface; use HiEvents\Repository\Interfaces\PromoCodeRepositoryInterface; +use HiEvents\Services\Application\Handlers\Order\DTO\CreateOrderPublicDTO; use HiEvents\Services\Domain\Order\OrderItemProcessingService; use HiEvents\Services\Domain\Order\OrderManagementService; -use HiEvents\Services\Handlers\Order\DTO\CreateOrderPublicDTO; use Illuminate\Database\DatabaseManager; use Illuminate\Validation\UnauthorizedException; use Throwable; @@ -64,7 +64,7 @@ public function handle( $orderItems = $this->orderItemProcessingService->process( order: $order, - ticketsOrderDetails: $createOrderPublicDTO->tickets, + productsOrderDetails: $createOrderPublicDTO->products, event: $event, promoCode: $promoCode, ); diff --git a/backend/app/Services/Handlers/Order/DTO/CancelOrderDTO.php b/backend/app/Services/Application/Handlers/Order/DTO/CancelOrderDTO.php similarity index 76% rename from backend/app/Services/Handlers/Order/DTO/CancelOrderDTO.php rename to backend/app/Services/Application/Handlers/Order/DTO/CancelOrderDTO.php index 015a5199..7f6069fb 100644 --- a/backend/app/Services/Handlers/Order/DTO/CancelOrderDTO.php +++ b/backend/app/Services/Application/Handlers/Order/DTO/CancelOrderDTO.php @@ -1,6 +1,6 @@ $attendees + * @param Collection $products */ public function __construct( public CompleteOrderOrderDTO $order, - #[CollectionOf(CompleteOrderAttendeeDTO::class)] - public Collection $attendees + #[CollectionOf(CompleteOrderProductDataDTO::class)] + public Collection $products ) { } diff --git a/backend/app/Services/Handlers/Order/DTO/CompleteOrderOrderDTO.php b/backend/app/Services/Application/Handlers/Order/DTO/CompleteOrderOrderDTO.php similarity index 92% rename from backend/app/Services/Handlers/Order/DTO/CompleteOrderOrderDTO.php rename to backend/app/Services/Application/Handlers/Order/DTO/CompleteOrderOrderDTO.php index 97793638..91c0f97a 100644 --- a/backend/app/Services/Handlers/Order/DTO/CompleteOrderOrderDTO.php +++ b/backend/app/Services/Application/Handlers/Order/DTO/CompleteOrderOrderDTO.php @@ -1,6 +1,6 @@ first_name !== null + && $this->last_name !== null + && $this->email !== null; + } +} diff --git a/backend/app/Services/Handlers/Order/DTO/CreateOrderPublicDTO.php b/backend/app/Services/Application/Handlers/Order/DTO/CreateOrderPublicDTO.php similarity index 73% rename from backend/app/Services/Handlers/Order/DTO/CreateOrderPublicDTO.php rename to backend/app/Services/Application/Handlers/Order/DTO/CreateOrderPublicDTO.php index 17bf413d..820513bc 100644 --- a/backend/app/Services/Handlers/Order/DTO/CreateOrderPublicDTO.php +++ b/backend/app/Services/Application/Handlers/Order/DTO/CreateOrderPublicDTO.php @@ -1,6 +1,6 @@ + * @var Collection */ - public readonly Collection $tickets, + public readonly Collection $products, public readonly bool $is_user_authenticated, public readonly string $session_identifier, public readonly ?string $order_locale = null, diff --git a/backend/app/Services/Application/Handlers/Order/DTO/CreatedProductDataDTO.php b/backend/app/Services/Application/Handlers/Order/DTO/CreatedProductDataDTO.php new file mode 100644 index 00000000..fc9573e1 --- /dev/null +++ b/backend/app/Services/Application/Handlers/Order/DTO/CreatedProductDataDTO.php @@ -0,0 +1,15 @@ +prices->map(fn(ProductPriceDTO $price) => ProductPriceDomainObject::hydrateFromArray([ + ProductPriceDomainObjectAbstract::PRICE => $productsData->type === ProductPriceType::FREE ? 0.00 : $price->price, + ProductPriceDomainObjectAbstract::LABEL => $price->label, + ProductPriceDomainObjectAbstract::SALE_START_DATE => $price->sale_start_date, + ProductPriceDomainObjectAbstract::SALE_END_DATE => $price->sale_end_date, + ProductPriceDomainObjectAbstract::INITIAL_QUANTITY_AVAILABLE => $price->initial_quantity_available, + ProductPriceDomainObjectAbstract::IS_HIDDEN => $price->is_hidden, + ])); + + $category = $this->getProductCategoryService->getCategory( + categoryId: $productsData->product_category_id, + eventId: $productsData->event_id + ); + + return $this->productCreateService->createProduct( + product: (new ProductDomainObject()) + ->setTitle($productsData->title) + ->setType($productsData->type->name) + ->setOrder($productsData->order) + ->setSaleStartDate($productsData->sale_start_date) + ->setSaleEndDate($productsData->sale_end_date) + ->setMaxPerOrder($productsData->max_per_order) + ->setDescription($productsData->description) + ->setMinPerOrder($productsData->min_per_order) + ->setIsHidden($productsData->is_hidden) + ->setStartCollapsed($productsData->start_collapsed) + ->setHideBeforeSaleStartDate($productsData->hide_before_sale_start_date) + ->setHideAfterSaleEndDate($productsData->hide_after_sale_end_date) + ->setHideWhenSoldOut($productsData->hide_when_sold_out) + ->setShowQuantityRemaining($productsData->show_quantity_remaining) + ->setIsHiddenWithoutPromoCode($productsData->is_hidden_without_promo_code) + ->setProductPrices($productPrices) + ->setEventId($productsData->event_id) + ->setProductType($productsData->product_type->name) + ->setProductCategoryId($category->getId()), + accountId: $productsData->account_id, + taxAndFeeIds: $productsData->tax_and_fee_ids, + ); + } +} diff --git a/backend/app/Services/Application/Handlers/Product/DTO/UpsertProductDTO.php b/backend/app/Services/Application/Handlers/Product/DTO/UpsertProductDTO.php new file mode 100644 index 00000000..6fa9f975 --- /dev/null +++ b/backend/app/Services/Application/Handlers/Product/DTO/UpsertProductDTO.php @@ -0,0 +1,45 @@ +deleteProductService->deleteProduct($productId, $eventId); + } +} diff --git a/backend/app/Services/Application/Handlers/Product/EditProductHandler.php b/backend/app/Services/Application/Handlers/Product/EditProductHandler.php new file mode 100644 index 00000000..dce9ae99 --- /dev/null +++ b/backend/app/Services/Application/Handlers/Product/EditProductHandler.php @@ -0,0 +1,153 @@ +databaseManager->transaction(function () use ($productsData) { + $where = [ + 'event_id' => $productsData->event_id, + 'id' => $productsData->product_id, + ]; + + $product = $this->updateProduct($productsData, $where); + + $this->addTaxes($product, $productsData); + + $this->priceUpdateService->updatePrices( + $product, + $productsData, + $product->getProductPrices(), + $this->eventRepository->findById($productsData->event_id) + ); + + return $this->productRepository + ->loadRelation(ProductPriceDomainObject::class) + ->findById($product->getId()); + }); + } + + /** + * @throws CannotChangeProductTypeException + */ + private function updateProduct(UpsertProductDTO $productsData, array $where): ProductDomainObject + { + $event = $this->eventRepository->findById($productsData->event_id); + + $this->validateChangeInProductType($productsData); + + $productCategory = $this->getProductCategoryService->getCategory( + $productsData->product_category_id, + $productsData->event_id, + ); + + $this->productRepository->updateWhere( + attributes: [ + 'title' => $productsData->title, + 'type' => $productsData->type->name, + 'order' => $this->productOrderingService->getOrderForNewProduct( + eventId: $productsData->event_id, + productCategoryId: $productCategory->getId(), + ), + 'sale_start_date' => $productsData->sale_start_date + ? DateHelper::convertToUTC($productsData->sale_start_date, $event->getTimezone()) + : null, + 'sale_end_date' => $productsData->sale_end_date + ? DateHelper::convertToUTC($productsData->sale_end_date, $event->getTimezone()) + : null, + 'max_per_order' => $productsData->max_per_order, + 'description' => $this->purifier->purify($productsData->description), + 'min_per_order' => $productsData->min_per_order, + 'is_hidden' => $productsData->is_hidden, + 'start_collapsed' => $productsData->start_collapsed, + 'hide_before_sale_start_date' => $productsData->hide_before_sale_start_date, + 'hide_after_sale_end_date' => $productsData->hide_after_sale_end_date, + 'hide_when_sold_out' => $productsData->hide_when_sold_out, + 'show_quantity_remaining' => $productsData->show_quantity_remaining, + 'is_hidden_without_promo_code' => $productsData->is_hidden_without_promo_code, + 'product_type' => $productsData->product_type->name, + 'product_category_id' => $productCategory->getId(), + ], + where: $where + ); + + return $this->productRepository + ->loadRelation(ProductPriceDomainObject::class) + ->findFirstWhere($where); + } + + /** + * @throws Exception + */ + private function addTaxes(ProductDomainObject $product, UpsertProductDTO $productsData): void + { + $this->taxAndProductAssociationService->addTaxesToProduct( + new TaxAndProductAssociateParams( + productId: $product->getId(), + accountId: $productsData->account_id, + taxAndFeeIds: $productsData->tax_and_fee_ids, + ) + ); + } + + /** + * @throws CannotChangeProductTypeException + * @todo - We should probably check reserved products here as well + */ + private function validateChangeInProductType(UpsertProductDTO $productsData): void + { + $product = $this->productRepository + ->loadRelation(ProductPriceDomainObject::class) + ->findById($productsData->product_id); + + $quantitySold = $product->getProductPrices() + ->sum(fn(ProductPriceDomainObject $price) => $price->getQuantitySold()); + + if ($product->getType() !== $productsData->type->name && $quantitySold > 0) { + throw new CannotChangeProductTypeException( + __('Product type cannot be changed as products have been registered for this type') + ); + } + } +} diff --git a/backend/app/Services/Application/Handlers/Product/GetProductsHandler.php b/backend/app/Services/Application/Handlers/Product/GetProductsHandler.php new file mode 100644 index 00000000..73ae5c74 --- /dev/null +++ b/backend/app/Services/Application/Handlers/Product/GetProductsHandler.php @@ -0,0 +1,37 @@ +productRepository + ->loadRelation(ProductPriceDomainObject::class) + ->loadRelation(TaxAndFeesDomainObject::class) + ->findByEventId($eventId, $queryParamsDTO); + + $filteredProducts = $this->productFilterService->filter( + productsCategories: $productPaginator->getCollection(), + hideSoldOutProducts: false, + ); + + $productPaginator->setCollection($filteredProducts); + + return $productPaginator; + } +} diff --git a/backend/app/Services/Application/Handlers/Product/SortProductsHandler.php b/backend/app/Services/Application/Handlers/Product/SortProductsHandler.php new file mode 100644 index 00000000..557bcaef --- /dev/null +++ b/backend/app/Services/Application/Handlers/Product/SortProductsHandler.php @@ -0,0 +1,73 @@ +productCategoryRepository + ->loadRelation(ProductDomainObject::class) + ->findWhere(['event_id' => $eventId]); + + $existingCategoryIds = $categories->map(fn($category) => $category->getId())->toArray(); + $existingProductIds = $categories->flatMap(fn($category) => $category->products->map(fn($product) => $product->getId()))->toArray(); + + $orderedCategoryIds = collect($sortData)->pluck('product_category_id')->toArray(); + $orderedProductIds = collect($sortData) + ->flatMap(fn($category) => collect($category['sorted_products'])->pluck('id')) + ->toArray(); + + if (array_diff($existingCategoryIds, $orderedCategoryIds) || array_diff($orderedCategoryIds, $existingCategoryIds)) { + throw new ResourceConflictException( + __('The ordered category IDs must exactly match all categories for the event without missing or extra IDs.') + ); + } + + if (array_diff($existingProductIds, $orderedProductIds) || array_diff($orderedProductIds, $existingProductIds)) { + throw new ResourceConflictException( + __('The ordered product IDs must exactly match all products for the event without missing or extra IDs.') + ); + } + + $productUpdates = []; + $categoryUpdates = []; + + foreach ($sortData as $categoryIndex => $category) { + $categoryId = $category['product_category_id']; + $categoryUpdates[] = [ + 'id' => $categoryId, + 'order' => $categoryIndex + 1, + ]; + + foreach ($category['sorted_products'] as $productIndex => $product) { + $productUpdates[] = [ + 'id' => $product['id'], + 'order' => $productIndex + 1, + 'product_category_id' => $categoryId, + ]; + } + } + + $this->productRepository->bulkUpdateProductsAndCategories( + eventId: $eventId, + productUpdates: $productUpdates, + categoryUpdates: $categoryUpdates, + ); + } +} diff --git a/backend/app/Services/Application/Handlers/ProductCategory/CreateProductCategoryHandler.php b/backend/app/Services/Application/Handlers/ProductCategory/CreateProductCategoryHandler.php new file mode 100644 index 00000000..73784755 --- /dev/null +++ b/backend/app/Services/Application/Handlers/ProductCategory/CreateProductCategoryHandler.php @@ -0,0 +1,27 @@ +productCategoryService->createCategory( + name: $dto->name, + isHidden: $dto->is_hidden, + eventId: $dto->event_id, + description: $dto->description, + noProductsMessage: $dto->no_products_message ?? __('There are no products available in this category'), + ); + } +} diff --git a/backend/app/Services/Application/Handlers/ProductCategory/DTO/UpsertProductCategoryDTO.php b/backend/app/Services/Application/Handlers/ProductCategory/DTO/UpsertProductCategoryDTO.php new file mode 100644 index 00000000..01e2d8e1 --- /dev/null +++ b/backend/app/Services/Application/Handlers/ProductCategory/DTO/UpsertProductCategoryDTO.php @@ -0,0 +1,19 @@ +deleteProductCategoryService->deleteProductCategory($productCategoryId, $eventId); + } +} diff --git a/backend/app/Services/Application/Handlers/ProductCategory/EditProductCategoryHandler.php b/backend/app/Services/Application/Handlers/ProductCategory/EditProductCategoryHandler.php new file mode 100644 index 00000000..105adde1 --- /dev/null +++ b/backend/app/Services/Application/Handlers/ProductCategory/EditProductCategoryHandler.php @@ -0,0 +1,34 @@ +productCategoryRepository->updateWhere( + attributes: [ + 'name' => $dto->name, + 'is_hidden' => $dto->is_hidden, + 'description' => $dto->description, + 'no_products_message' => $dto->no_products_message ?? __('There are no products available in this category'), + ], + where: [ + 'id' => $dto->product_category_id, + 'event_id' => $dto->event_id, + ], + ); + + return $this->productCategoryRepository->findById($dto->product_category_id); + } +} diff --git a/backend/app/Services/Application/Handlers/ProductCategory/GetProductCategoriesHandler.php b/backend/app/Services/Application/Handlers/ProductCategory/GetProductCategoriesHandler.php new file mode 100644 index 00000000..ccd815bb --- /dev/null +++ b/backend/app/Services/Application/Handlers/ProductCategory/GetProductCategoriesHandler.php @@ -0,0 +1,49 @@ +productCategoryRepository + ->loadRelation(new Relationship( + domainObject: ProductDomainObject::class, + nested: [ + new Relationship(ProductPriceDomainObject::class), + new Relationship(TaxAndFeesDomainObject::class), + ], + orderAndDirections: [ + new OrderAndDirection( + order: ProductDomainObjectAbstract::ORDER, + ), + ], + )) + ->findWhere( + where: [ + 'event_id' => $eventId, + ], + orderAndDirections: [ + new OrderAndDirection( + order: ProductCategoryDomainObjectAbstract::ORDER, + ), + ], + ); + } +} diff --git a/backend/app/Services/Application/Handlers/ProductCategory/GetProductCategoryHandler.php b/backend/app/Services/Application/Handlers/ProductCategory/GetProductCategoryHandler.php new file mode 100644 index 00000000..0df97694 --- /dev/null +++ b/backend/app/Services/Application/Handlers/ProductCategory/GetProductCategoryHandler.php @@ -0,0 +1,44 @@ +productCategoryRepository + ->loadRelation(new Relationship( + domainObject: ProductDomainObject::class, + nested: [ + new Relationship(ProductPriceDomainObject::class), + new Relationship(TaxAndFeesDomainObject::class), + ], + orderAndDirections: [ + new OrderAndDirection( + order: ProductDomainObjectAbstract::ORDER, + ), + ], + )) + ->findFirstWhere( + where: [ + 'event_id' => $eventId, + 'id' => $productCategoryId, + ] + ); + } +} diff --git a/backend/app/Services/Handlers/PromoCode/CreatePromoCodeHandler.php b/backend/app/Services/Application/Handlers/PromoCode/CreatePromoCodeHandler.php similarity index 73% rename from backend/app/Services/Handlers/PromoCode/CreatePromoCodeHandler.php rename to backend/app/Services/Application/Handlers/PromoCode/CreatePromoCodeHandler.php index 84e954a3..37d3e9e8 100644 --- a/backend/app/Services/Handlers/PromoCode/CreatePromoCodeHandler.php +++ b/backend/app/Services/Application/Handlers/PromoCode/CreatePromoCodeHandler.php @@ -1,12 +1,12 @@ setDiscount($promoCodeDTO->discount) ->setExpiryDate($promoCodeDTO->expiry_date) ->setMaxAllowedUsages($promoCodeDTO->max_allowed_usages) - ->setApplicableTicketIds($promoCodeDTO->applicable_ticket_ids) + ->setApplicableProductIds($promoCodeDTO->applicable_product_ids) ); } } diff --git a/backend/app/Services/Handlers/PromoCode/DTO/DeletePromoCodeDTO.php b/backend/app/Services/Application/Handlers/PromoCode/DTO/DeletePromoCodeDTO.php similarity index 79% rename from backend/app/Services/Handlers/PromoCode/DTO/DeletePromoCodeDTO.php rename to backend/app/Services/Application/Handlers/PromoCode/DTO/DeletePromoCodeDTO.php index 72e02245..26768a3b 100644 --- a/backend/app/Services/Handlers/PromoCode/DTO/DeletePromoCodeDTO.php +++ b/backend/app/Services/Application/Handlers/PromoCode/DTO/DeletePromoCodeDTO.php @@ -1,6 +1,6 @@ eventTicketValidationService->validateTicketIds( - ticketIds: $promoCodeDTO->applicable_ticket_ids, + $this->eventProductValidationService->validateProductIds( + productIds: $promoCodeDTO->applicable_product_ids, eventId: $promoCodeDTO->event_id ); @@ -57,7 +57,7 @@ public function handle(int $promoCodeId, UpsertPromoCodeDTO $promoCodeDTO): Prom ? DateHelper::convertToUTC($promoCodeDTO->expiry_date, $event->getTimezone()) : null, PromoCodeDomainObjectAbstract::MAX_ALLOWED_USAGES => $promoCodeDTO->max_allowed_usages, - PromoCodeDomainObjectAbstract::APPLICABLE_TICKET_IDS => $promoCodeDTO->applicable_ticket_ids, + PromoCodeDomainObjectAbstract::APPLICABLE_PRODUCT_IDS => $promoCodeDTO->applicable_product_ids, ]); } } diff --git a/backend/app/Services/Handlers/Question/CreateQuestionHandler.php b/backend/app/Services/Application/Handlers/Question/CreateQuestionHandler.php similarity index 86% rename from backend/app/Services/Handlers/Question/CreateQuestionHandler.php rename to backend/app/Services/Application/Handlers/Question/CreateQuestionHandler.php index a8f641e7..8a3b9d64 100644 --- a/backend/app/Services/Handlers/Question/CreateQuestionHandler.php +++ b/backend/app/Services/Application/Handlers/Question/CreateQuestionHandler.php @@ -1,10 +1,10 @@ createQuestionService->createQuestion( $question, - $createQuestionDTO->ticket_ids, + $createQuestionDTO->product_ids, ); } } diff --git a/backend/app/Services/Handlers/Question/DTO/UpsertQuestionDTO.php b/backend/app/Services/Application/Handlers/Question/DTO/UpsertQuestionDTO.php similarity index 84% rename from backend/app/Services/Handlers/Question/DTO/UpsertQuestionDTO.php rename to backend/app/Services/Application/Handlers/Question/DTO/UpsertQuestionDTO.php index c1034540..e8386900 100644 --- a/backend/app/Services/Handlers/Question/DTO/UpsertQuestionDTO.php +++ b/backend/app/Services/Application/Handlers/Question/DTO/UpsertQuestionDTO.php @@ -1,6 +1,6 @@ editQuestionService->editQuestion( question: $question, - ticketIds: $createQuestionDTO->ticket_ids, + productIds: $createQuestionDTO->product_ids, ); } } diff --git a/backend/app/Services/Handlers/Question/SortQuestionsHandler.php b/backend/app/Services/Application/Handlers/Question/SortQuestionsHandler.php similarity index 89% rename from backend/app/Services/Handlers/Question/SortQuestionsHandler.php rename to backend/app/Services/Application/Handlers/Question/SortQuestionsHandler.php index 379271f9..e0387bec 100644 --- a/backend/app/Services/Handlers/Question/SortQuestionsHandler.php +++ b/backend/app/Services/Application/Handlers/Question/SortQuestionsHandler.php @@ -1,6 +1,6 @@ questionRepository->findWhere([ 'event_id' => $eventId, ]) - ->map(fn($ticket) => $ticket->getId()) + ->map(fn($product) => $product->getId()) ->toArray(); $extraInOrdered = array_diff($orderedQuestionIds, $questionIdResult); diff --git a/backend/app/Services/Application/Handlers/Reports/DTO/GetReportDTO.php b/backend/app/Services/Application/Handlers/Reports/DTO/GetReportDTO.php new file mode 100644 index 00000000..295c9b4d --- /dev/null +++ b/backend/app/Services/Application/Handlers/Reports/DTO/GetReportDTO.php @@ -0,0 +1,18 @@ +reportServiceFactory + ->create($reportData->reportType) + ->generateReport( + eventId: $reportData->eventId, + startDate: $reportData->startDate ? Carbon::parse($reportData->startDate) : null, + endDate: $reportData->endDate ? Carbon::parse($reportData->endDate) : null, + ); + } +} diff --git a/backend/app/Services/Handlers/TaxAndFee/CreateTaxOrFeeHandler.php b/backend/app/Services/Application/Handlers/TaxAndFee/CreateTaxOrFeeHandler.php similarity index 93% rename from backend/app/Services/Handlers/TaxAndFee/CreateTaxOrFeeHandler.php rename to backend/app/Services/Application/Handlers/TaxAndFee/CreateTaxOrFeeHandler.php index f8594b18..faaa849b 100644 --- a/backend/app/Services/Handlers/TaxAndFee/CreateTaxOrFeeHandler.php +++ b/backend/app/Services/Application/Handlers/TaxAndFee/CreateTaxOrFeeHandler.php @@ -1,12 +1,12 @@ currentAccessToken()) { + if (Auth::user()->currentAccessToken() instanceof TransientToken) { + // assume logged in + return auth()->guard('api')->payload()->get('account_id'); + } else { + return Auth::user()->currentAccessToken()->account_id; + } + } + try { /** @var Payload $payload */ $payload = $this->authManager->payload(); diff --git a/backend/app/Services/Domain/CapacityAssignment/CapacityAssignmentTicketAssociationService.php b/backend/app/Services/Domain/CapacityAssignment/CapacityAssignmentProductAssociationService.php similarity index 50% rename from backend/app/Services/Domain/CapacityAssignment/CapacityAssignmentTicketAssociationService.php rename to backend/app/Services/Domain/CapacityAssignment/CapacityAssignmentProductAssociationService.php index c5a0f5a2..e7f1b68d 100644 --- a/backend/app/Services/Domain/CapacityAssignment/CapacityAssignmentTicketAssociationService.php +++ b/backend/app/Services/Domain/CapacityAssignment/CapacityAssignmentProductAssociationService.php @@ -2,52 +2,52 @@ namespace HiEvents\Services\Domain\CapacityAssignment; -use HiEvents\Repository\Interfaces\TicketRepositoryInterface; +use HiEvents\Repository\Interfaces\ProductRepositoryInterface; use Illuminate\Database\DatabaseManager; -class CapacityAssignmentTicketAssociationService +class CapacityAssignmentProductAssociationService { public function __construct( - private readonly TicketRepositoryInterface $ticketRepository, - public readonly DatabaseManager $databaseManager, + private readonly ProductRepositoryInterface $productRepository, + public readonly DatabaseManager $databaseManager, ) { } - public function addCapacityToTickets( + public function addCapacityToProducts( int $capacityAssignmentId, - ?array $ticketIds, + ?array $productIds, bool $removePreviousAssignments = true ): void { - $this->databaseManager->transaction(function () use ($capacityAssignmentId, $ticketIds, $removePreviousAssignments) { - $this->associateTicketsWithCapacityAssignment( + $this->databaseManager->transaction(function () use ($capacityAssignmentId, $productIds, $removePreviousAssignments) { + $this->associateProductsWithCapacityAssignment( capacityAssignmentId: $capacityAssignmentId, - ticketIds: $ticketIds, + productIds: $productIds, removePreviousAssignments: $removePreviousAssignments, ); }); } - private function associateTicketsWithCapacityAssignment( + private function associateProductsWithCapacityAssignment( int $capacityAssignmentId, - ?array $ticketIds, + ?array $productIds, bool $removePreviousAssignments = true ): void { - if (empty($ticketIds)) { + if (empty($productIds)) { return; } if ($removePreviousAssignments) { - $this->ticketRepository->removeCapacityAssignmentFromTickets( + $this->productRepository->removeCapacityAssignmentFromProducts( capacityAssignmentId: $capacityAssignmentId, ); } - $this->ticketRepository->addCapacityAssignmentToTickets( + $this->productRepository->addCapacityAssignmentToProducts( capacityAssignmentId: $capacityAssignmentId, - ticketIds: array_unique($ticketIds), + productIds: array_unique($productIds), ); } } diff --git a/backend/app/Services/Domain/CapacityAssignment/CreateCapacityAssignmentService.php b/backend/app/Services/Domain/CapacityAssignment/CreateCapacityAssignmentService.php index b5bb21a8..4c7e68c6 100644 --- a/backend/app/Services/Domain/CapacityAssignment/CreateCapacityAssignmentService.php +++ b/backend/app/Services/Domain/CapacityAssignment/CreateCapacityAssignmentService.php @@ -5,11 +5,11 @@ use HiEvents\DomainObjects\CapacityAssignmentDomainObject; use HiEvents\DomainObjects\Enums\CapacityAssignmentAppliesTo; use HiEvents\DomainObjects\Generated\CapacityAssignmentDomainObjectAbstract; -use HiEvents\DomainObjects\TicketPriceDomainObject; +use HiEvents\DomainObjects\ProductPriceDomainObject; use HiEvents\Repository\Interfaces\CapacityAssignmentRepositoryInterface; -use HiEvents\Repository\Interfaces\TicketPriceRepositoryInterface; -use HiEvents\Services\Domain\Ticket\EventTicketValidationService; -use HiEvents\Services\Domain\Ticket\Exception\UnrecognizedTicketIdException; +use HiEvents\Repository\Interfaces\ProductPriceRepositoryInterface; +use HiEvents\Services\Domain\Product\EventProductValidationService; +use HiEvents\Services\Domain\Product\Exception\UnrecognizedProductIdException; use Illuminate\Database\DatabaseManager; class CreateCapacityAssignmentService @@ -17,32 +17,32 @@ class CreateCapacityAssignmentService public function __construct( private readonly DatabaseManager $databaseManager, private readonly CapacityAssignmentRepositoryInterface $capacityAssignmentRepository, - private readonly EventTicketValidationService $eventTicketValidationService, - private readonly CapacityAssignmentTicketAssociationService $capacityAssignmentTicketAssociationService, - private readonly TicketPriceRepositoryInterface $ticketPriceRepository, + private readonly EventProductValidationService $eventProductValidationService, + private readonly CapacityAssignmentProductAssociationService $capacityAssignmentProductAssociationService, + private readonly ProductPriceRepositoryInterface $productPriceRepository, ) { } /** - * @throws UnrecognizedTicketIdException + * @throws UnrecognizedProductIdException */ public function createCapacityAssignment( CapacityAssignmentDomainObject $capacityAssignment, - array $ticketIds, + array $productIds, ): CapacityAssignmentDomainObject { - $this->eventTicketValidationService->validateTicketIds($ticketIds, $capacityAssignment->getEventId()); + $this->eventProductValidationService->validateProductIds($productIds, $capacityAssignment->getEventId()); - return $this->persistAssignmentAndAssociateTickets($capacityAssignment, $ticketIds); + return $this->persistAssignmentAndAssociateProducts($capacityAssignment, $productIds); } - private function persistAssignmentAndAssociateTickets( + private function persistAssignmentAndAssociateProducts( CapacityAssignmentDomainObject $capacityAssignment, - ?array $ticketIds, + ?array $productIds, ): CapacityAssignmentDomainObject { - return $this->databaseManager->transaction(function () use ($capacityAssignment, $ticketIds) { + return $this->databaseManager->transaction(function () use ($capacityAssignment, $productIds) { /** @var CapacityAssignmentDomainObject $capacityAssignment */ $capacityAssignment = $this->capacityAssignmentRepository->create([ CapacityAssignmentDomainObjectAbstract::NAME => $capacityAssignment->getName(), @@ -50,13 +50,13 @@ private function persistAssignmentAndAssociateTickets( CapacityAssignmentDomainObjectAbstract::CAPACITY => $capacityAssignment->getCapacity(), CapacityAssignmentDomainObjectAbstract::APPLIES_TO => $capacityAssignment->getAppliesTo(), CapacityAssignmentDomainObjectAbstract::STATUS => $capacityAssignment->getStatus(), - CapacityAssignmentDomainObjectAbstract::USED_CAPACITY => $this->getUsedCapacity($ticketIds), + CapacityAssignmentDomainObjectAbstract::USED_CAPACITY => $this->getUsedCapacity($productIds), ]); - if ($capacityAssignment->getAppliesTo() === CapacityAssignmentAppliesTo::TICKETS->name) { - $this->capacityAssignmentTicketAssociationService->addCapacityToTickets( + if ($capacityAssignment->getAppliesTo() === CapacityAssignmentAppliesTo::PRODUCTS->name) { + $this->capacityAssignmentProductAssociationService->addCapacityToProducts( capacityAssignmentId: $capacityAssignment->getId(), - ticketIds: $ticketIds, + productIds: $productIds, removePreviousAssignments: false, ); } @@ -65,10 +65,10 @@ private function persistAssignmentAndAssociateTickets( }); } - private function getUsedCapacity(array $ticketIds): int + private function getUsedCapacity(array $productIds): int { - $ticketPrices = $this->ticketPriceRepository->findWhereIn('ticket_id', $ticketIds); + $productPrices = $this->productPriceRepository->findWhereIn('product_id', $productIds); - return $ticketPrices->sum(fn(TicketPriceDomainObject $ticketPrice) => $ticketPrice->getQuantitySold()); + return $productPrices->sum(fn(ProductPriceDomainObject $productPrice) => $productPrice->getQuantitySold()); } } diff --git a/backend/app/Services/Domain/CapacityAssignment/Exception/TicketsDoNotBelongToEventException.php b/backend/app/Services/Domain/CapacityAssignment/Exception/ProductsDoNotBelongToEventException.php similarity index 61% rename from backend/app/Services/Domain/CapacityAssignment/Exception/TicketsDoNotBelongToEventException.php rename to backend/app/Services/Domain/CapacityAssignment/Exception/ProductsDoNotBelongToEventException.php index 8e2cbefb..1505af45 100644 --- a/backend/app/Services/Domain/CapacityAssignment/Exception/TicketsDoNotBelongToEventException.php +++ b/backend/app/Services/Domain/CapacityAssignment/Exception/ProductsDoNotBelongToEventException.php @@ -4,7 +4,7 @@ use Exception; -class TicketsDoNotBelongToEventException extends Exception +class ProductsDoNotBelongToEventException extends Exception { } diff --git a/backend/app/Services/Domain/CapacityAssignment/UpdateCapacityAssignmentService.php b/backend/app/Services/Domain/CapacityAssignment/UpdateCapacityAssignmentService.php index a0d0899a..b355cd00 100644 --- a/backend/app/Services/Domain/CapacityAssignment/UpdateCapacityAssignmentService.php +++ b/backend/app/Services/Domain/CapacityAssignment/UpdateCapacityAssignmentService.php @@ -6,8 +6,8 @@ use HiEvents\DomainObjects\Enums\CapacityAssignmentAppliesTo; use HiEvents\DomainObjects\Generated\CapacityAssignmentDomainObjectAbstract; use HiEvents\Repository\Interfaces\CapacityAssignmentRepositoryInterface; -use HiEvents\Services\Domain\Ticket\EventTicketValidationService; -use HiEvents\Services\Domain\Ticket\Exception\UnrecognizedTicketIdException; +use HiEvents\Services\Domain\Product\EventProductValidationService; +use HiEvents\Services\Domain\Product\Exception\UnrecognizedProductIdException; use Illuminate\Database\DatabaseManager; class UpdateCapacityAssignmentService @@ -15,33 +15,33 @@ class UpdateCapacityAssignmentService public function __construct( private readonly DatabaseManager $databaseManager, private readonly CapacityAssignmentRepositoryInterface $capacityAssignmentRepository, - private readonly EventTicketValidationService $eventTicketValidationService, - private readonly CapacityAssignmentTicketAssociationService $capacityAssignmentTicketAssociationService, + private readonly EventProductValidationService $eventProductValidationService, + private readonly CapacityAssignmentProductAssociationService $capacityAssignmentProductAssociationService, ) { } /** - * @throws UnrecognizedTicketIdException + * @throws UnrecognizedProductIdException */ public function updateCapacityAssignment( CapacityAssignmentDomainObject $capacityAssignment, - ?array $ticketIds = null, + ?array $productIds = null, ): CapacityAssignmentDomainObject { - if ($ticketIds !== null) { - $this->eventTicketValidationService->validateTicketIds($ticketIds, $capacityAssignment->getEventId()); + if ($productIds !== null) { + $this->eventProductValidationService->validateProductIds($productIds, $capacityAssignment->getEventId()); } - return $this->updateAssignmentAndAssociateTickets($capacityAssignment, $ticketIds); + return $this->updateAssignmentAndAssociateProducts($capacityAssignment, $productIds); } - private function updateAssignmentAndAssociateTickets( + private function updateAssignmentAndAssociateProducts( CapacityAssignmentDomainObject $capacityAssignment, - ?array $ticketIds + ?array $productIds ): CapacityAssignmentDomainObject { - return $this->databaseManager->transaction(function () use ($capacityAssignment, $ticketIds) { + return $this->databaseManager->transaction(function () use ($capacityAssignment, $productIds) { /** @var CapacityAssignmentDomainObject $capacityAssignment */ $this->capacityAssignmentRepository->updateWhere( attributes: [ @@ -57,10 +57,10 @@ private function updateAssignmentAndAssociateTickets( ] ); - if ($capacityAssignment->getAppliesTo() === CapacityAssignmentAppliesTo::TICKETS->name) { - $this->capacityAssignmentTicketAssociationService->addCapacityToTickets( + if ($capacityAssignment->getAppliesTo() === CapacityAssignmentAppliesTo::PRODUCTS->name) { + $this->capacityAssignmentProductAssociationService->addCapacityToProducts( capacityAssignmentId: $capacityAssignment->getId(), - ticketIds: $ticketIds, + productIds: $productIds, ); } diff --git a/backend/app/Services/Domain/CheckInList/CheckInListDataService.php b/backend/app/Services/Domain/CheckInList/CheckInListDataService.php index 3795ad04..f80a8c3c 100644 --- a/backend/app/Services/Domain/CheckInList/CheckInListDataService.php +++ b/backend/app/Services/Domain/CheckInList/CheckInListDataService.php @@ -7,7 +7,7 @@ use HiEvents\DomainObjects\CheckInListDomainObject; use HiEvents\DomainObjects\Generated\AttendeeDomainObjectAbstract; use HiEvents\DomainObjects\Generated\CheckInListDomainObjectAbstract; -use HiEvents\DomainObjects\TicketDomainObject; +use HiEvents\DomainObjects\ProductDomainObject; use HiEvents\Exceptions\CannotCheckInException; use HiEvents\Repository\Interfaces\AttendeeRepositoryInterface; use HiEvents\Repository\Interfaces\CheckInListRepositoryInterface; @@ -30,9 +30,9 @@ public function verifyAttendeeBelongsToCheckInList( AttendeeDomainObject $attendee, ): void { - $allowedTicketIds = $checkInList->getTickets()->map(fn($ticket) => $ticket->getId())->toArray() ?? []; + $allowedProductIds = $checkInList->getProducts()->map(fn($product) => $product->getId())->toArray() ?? []; - if (!in_array($attendee->getTicketId(), $allowedTicketIds, true)) { + if (!in_array($attendee->getProductId(), $allowedProductIds, true)) { throw new CannotCheckInException( __('Attendee :attendee_name is not allowed to check in using this check-in list', [ 'attendee_name' => $attendee->getFullName(), @@ -74,7 +74,7 @@ public function getAttendees(array $attendeePublicIds): Collection public function getCheckInList(string $checkInListUuid): CheckInListDomainObject { $checkInList = $this->checkInListRepository - ->loadRelation(TicketDomainObject::class) + ->loadRelation(ProductDomainObject::class) ->findFirstWhere([ CheckInListDomainObjectAbstract::SHORT_ID => $checkInListUuid, ]); diff --git a/backend/app/Services/Domain/CheckInList/CheckInListTicketAssociationService.php b/backend/app/Services/Domain/CheckInList/CheckInListProductAssociationService.php similarity index 50% rename from backend/app/Services/Domain/CheckInList/CheckInListTicketAssociationService.php rename to backend/app/Services/Domain/CheckInList/CheckInListProductAssociationService.php index 24c96c4e..2153c446 100644 --- a/backend/app/Services/Domain/CheckInList/CheckInListTicketAssociationService.php +++ b/backend/app/Services/Domain/CheckInList/CheckInListProductAssociationService.php @@ -2,52 +2,52 @@ namespace HiEvents\Services\Domain\CheckInList; -use HiEvents\Repository\Interfaces\TicketRepositoryInterface; +use HiEvents\Repository\Interfaces\ProductRepositoryInterface; use Illuminate\Database\DatabaseManager; -class CheckInListTicketAssociationService +class CheckInListProductAssociationService { public function __construct( - private readonly TicketRepositoryInterface $ticketRepository, - public readonly DatabaseManager $databaseManager, + private readonly ProductRepositoryInterface $productRepository, + public readonly DatabaseManager $databaseManager, ) { } - public function addCheckInListToTickets( + public function addCheckInListToProducts( int $checkInListId, - ?array $ticketIds, + ?array $productIds, bool $removePreviousAssignments = true ): void { - $this->databaseManager->transaction(function () use ($checkInListId, $ticketIds, $removePreviousAssignments) { - $this->associateTicketsWithCheckInList( + $this->databaseManager->transaction(function () use ($checkInListId, $productIds, $removePreviousAssignments) { + $this->associateProductsWithCheckInList( checkInListId: $checkInListId, - ticketIds: $ticketIds, + productIds: $productIds, removePreviousAssignments: $removePreviousAssignments, ); }); } - private function associateTicketsWithCheckInList( + private function associateProductsWithCheckInList( int $checkInListId, - ?array $ticketIds, + ?array $productIds, bool $removePreviousAssignments = true ): void { - if (empty($ticketIds)) { + if (empty($productIds)) { return; } if ($removePreviousAssignments) { - $this->ticketRepository->removeCheckInListFromTickets( + $this->productRepository->removeCheckInListFromProducts( checkInListId: $checkInListId, ); } - $this->ticketRepository->addCheckInListToTickets( + $this->productRepository->addCheckInListToProducts( checkInListId: $checkInListId, - ticketIds: array_unique($ticketIds), + productIds: array_unique($productIds), ); } } diff --git a/backend/app/Services/Domain/CheckInList/CreateAttendeeCheckInService.php b/backend/app/Services/Domain/CheckInList/CreateAttendeeCheckInService.php index 737619b5..127ab157 100644 --- a/backend/app/Services/Domain/CheckInList/CreateAttendeeCheckInService.php +++ b/backend/app/Services/Domain/CheckInList/CreateAttendeeCheckInService.php @@ -64,7 +64,7 @@ public function checkInAttendees( if ($attendee->getStatus() === AttendeeStatus::CANCELLED->name) { $errors->addError( key: $attendee->getPublicId(), - message: __('Attendee :attendee_name\'s ticket is cancelled', [ + message: __('Attendee :attendee_name\'s product is cancelled', [ 'attendee_name' => $attendee->getFullName(), ]) ); @@ -87,7 +87,7 @@ public function checkInAttendees( AttendeeCheckInDomainObjectAbstract::ATTENDEE_ID => $attendee->getId(), AttendeeCheckInDomainObjectAbstract::CHECK_IN_LIST_ID => $checkInList->getId(), AttendeeCheckInDomainObjectAbstract::IP_ADDRESS => $checkInUserIpAddress, - AttendeeCheckInDomainObjectAbstract::TICKET_ID => $attendee->getTicketId(), + AttendeeCheckInDomainObjectAbstract::PRODUCT_ID => $attendee->getProductId(), AttendeeCheckInDomainObjectAbstract::SHORT_ID => IdHelper::shortId(IdHelper::CHECK_IN_PREFIX), AttendeeCheckInDomainObjectAbstract::EVENT_ID => $checkInList->getEventId(), ]) diff --git a/backend/app/Services/Domain/CheckInList/CreateCheckInListService.php b/backend/app/Services/Domain/CheckInList/CreateCheckInListService.php index 93aa2938..a4e95f92 100644 --- a/backend/app/Services/Domain/CheckInList/CreateCheckInListService.php +++ b/backend/app/Services/Domain/CheckInList/CreateCheckInListService.php @@ -8,16 +8,16 @@ use HiEvents\Helper\IdHelper; use HiEvents\Repository\Interfaces\CheckInListRepositoryInterface; use HiEvents\Repository\Interfaces\EventRepositoryInterface; -use HiEvents\Services\Domain\Ticket\EventTicketValidationService; -use HiEvents\Services\Domain\Ticket\Exception\UnrecognizedTicketIdException; +use HiEvents\Services\Domain\Product\EventProductValidationService; +use HiEvents\Services\Domain\Product\Exception\UnrecognizedProductIdException; use Illuminate\Database\DatabaseManager; class CreateCheckInListService { public function __construct( private readonly CheckInListRepositoryInterface $checkInListRepository, - private readonly EventTicketValidationService $eventTicketValidationService, - private readonly CheckInListTicketAssociationService $checkInListTicketAssociationService, + private readonly EventProductValidationService $eventProductValidationService, + private readonly CheckInListProductAssociationService $checkInListProductAssociationService, private readonly DatabaseManager $databaseManager, private readonly EventRepositoryInterface $eventRepository, @@ -26,12 +26,12 @@ public function __construct( } /** - * @throws UnrecognizedTicketIdException + * @throws UnrecognizedProductIdException */ - public function createCheckInList(CheckInListDomainObject $checkInList, array $ticketIds): CheckInListDomainObject + public function createCheckInList(CheckInListDomainObject $checkInList, array $productIds): CheckInListDomainObject { - return $this->databaseManager->transaction(function () use ($checkInList, $ticketIds) { - $this->eventTicketValidationService->validateTicketIds($ticketIds, $checkInList->getEventId()); + return $this->databaseManager->transaction(function () use ($checkInList, $productIds) { + $this->eventProductValidationService->validateProductIds($productIds, $checkInList->getEventId()); $event = $this->eventRepository->findById($checkInList->getEventId()); $newCheckInList = $this->checkInListRepository->create([ @@ -47,9 +47,9 @@ public function createCheckInList(CheckInListDomainObject $checkInList, array $t CheckInListDomainObjectAbstract::SHORT_ID => IdHelper::shortId(IdHelper::CHECK_IN_LIST_PREFIX), ]); - $this->checkInListTicketAssociationService->addCheckInListToTickets( + $this->checkInListProductAssociationService->addCheckInListToProducts( checkInListId: $newCheckInList->getId(), - ticketIds: $ticketIds, + productIds: $productIds, removePreviousAssignments: false, ); diff --git a/backend/app/Services/Domain/CheckInList/UpdateCheckInListService.php b/backend/app/Services/Domain/CheckInList/UpdateCheckInListService.php index b26fd8b6..11a441de 100644 --- a/backend/app/Services/Domain/CheckInList/UpdateCheckInListService.php +++ b/backend/app/Services/Domain/CheckInList/UpdateCheckInListService.php @@ -7,16 +7,16 @@ use HiEvents\Helper\DateHelper; use HiEvents\Repository\Interfaces\CheckInListRepositoryInterface; use HiEvents\Repository\Interfaces\EventRepositoryInterface; -use HiEvents\Services\Domain\Ticket\EventTicketValidationService; -use HiEvents\Services\Domain\Ticket\Exception\UnrecognizedTicketIdException; +use HiEvents\Services\Domain\Product\EventProductValidationService; +use HiEvents\Services\Domain\Product\Exception\UnrecognizedProductIdException; use Illuminate\Database\DatabaseManager; class UpdateCheckInListService { public function __construct( private readonly DatabaseManager $databaseManager, - private readonly EventTicketValidationService $eventTicketValidationService, - private readonly CheckInListTicketAssociationService $checkInListTicketAssociationService, + private readonly EventProductValidationService $eventProductValidationService, + private readonly CheckInListProductAssociationService $checkInListProductAssociationService, private readonly CheckInListRepositoryInterface $checkInListRepository, private readonly EventRepositoryInterface $eventRepository, ) @@ -24,12 +24,12 @@ public function __construct( } /** - * @throws UnrecognizedTicketIdException + * @throws UnrecognizedProductIdException */ - public function updateCheckInList(CheckInListDomainObject $checkInList, array $ticketIds): CheckInListDomainObject + public function updateCheckInList(CheckInListDomainObject $checkInList, array $productIds): CheckInListDomainObject { - return $this->databaseManager->transaction(function () use ($checkInList, $ticketIds) { - $this->eventTicketValidationService->validateTicketIds($ticketIds, $checkInList->getEventId()); + return $this->databaseManager->transaction(function () use ($checkInList, $productIds) { + $this->eventProductValidationService->validateProductIds($productIds, $checkInList->getEventId()); $event = $this->eventRepository->findById($checkInList->getEventId()); $this->checkInListRepository->updateWhere( @@ -50,9 +50,9 @@ public function updateCheckInList(CheckInListDomainObject $checkInList, array $t ] ); - $this->checkInListTicketAssociationService->addCheckInListToTickets( + $this->checkInListProductAssociationService->addCheckInListToProducts( checkInListId: $checkInList->getId(), - ticketIds: $ticketIds, + productIds: $productIds, ); return $this->checkInListRepository->findFirstWhere( diff --git a/backend/app/Services/Domain/Event/CreateEventImageService.php b/backend/app/Services/Domain/Event/CreateEventImageService.php index a2b6eb73..7c9f8557 100644 --- a/backend/app/Services/Domain/Event/CreateEventImageService.php +++ b/backend/app/Services/Domain/Event/CreateEventImageService.php @@ -43,7 +43,7 @@ public function createImage( image: $image, entityId: $eventId, entityType: EventDomainObject::class, - imageType: EventImageType::EVENT_COVER->name, + imageType: $type->name, ); }); } diff --git a/backend/app/Services/Domain/Event/CreateEventService.php b/backend/app/Services/Domain/Event/CreateEventService.php index 4d8fdd86..98e6c6a8 100644 --- a/backend/app/Services/Domain/Event/CreateEventService.php +++ b/backend/app/Services/Domain/Event/CreateEventService.php @@ -13,6 +13,7 @@ use HiEvents\Repository\Interfaces\EventSettingsRepositoryInterface; use HiEvents\Repository\Interfaces\EventStatisticRepositoryInterface; use HiEvents\Repository\Interfaces\OrganizerRepositoryInterface; +use HiEvents\Services\Domain\ProductCategory\CreateProductCategoryService; use HTMLPurifier; use Illuminate\Database\DatabaseManager; use Throwable; @@ -26,6 +27,7 @@ public function __construct( private readonly DatabaseManager $databaseManager, private readonly EventStatisticRepositoryInterface $eventStatisticsRepository, private readonly HTMLPurifier $purifier, + private readonly CreateProductCategoryService $createProductCategoryService, ) { } @@ -55,6 +57,8 @@ public function createEvent( $this->createEventStatistics($event); + $this->createDefaultProductCategory($event); + $this->databaseManager->commit(); return $event; @@ -104,7 +108,7 @@ private function createEventStatistics(EventDomainObject $event): void { $this->eventStatisticsRepository->create([ 'event_id' => $event->getId(), - 'tickets_sold' => 0, + 'products_sold' => 0, 'sales_total_gross' => 0, 'sales_total_before_additions' => 0, 'total_tax' => 0, @@ -143,4 +147,15 @@ private function createEventSettings( 'support_email' => $organizer->getEmail(), ]); } + + private function createDefaultProductCategory(EventDomainObject $event): void + { + $this->createProductCategoryService->createCategory( + name: __('Tickets'), + isHidden: false, + eventId: $event->getId(), + description: null, + noProductsMessage: __('There are no tickets available for this event.'), + ); + } } diff --git a/backend/app/Services/Domain/Event/DTO/DuplicateEventDataDTO.php b/backend/app/Services/Domain/Event/DTO/DuplicateEventDataDTO.php index 83262ea5..b0604723 100644 --- a/backend/app/Services/Domain/Event/DTO/DuplicateEventDataDTO.php +++ b/backend/app/Services/Domain/Event/DTO/DuplicateEventDataDTO.php @@ -11,7 +11,7 @@ public function __construct( public int $accountId, public string $title, public string $startDate, - public bool $duplicateTickets = true, + public bool $duplicateProducts = true, public bool $duplicateQuestions = true, public bool $duplicateSettings = true, public bool $duplicatePromoCodes = true, diff --git a/backend/app/Services/Domain/Event/DTO/EventDailyStatsResponseDTO.php b/backend/app/Services/Domain/Event/DTO/EventDailyStatsResponseDTO.php index f6d88472..e15c8fd1 100644 --- a/backend/app/Services/Domain/Event/DTO/EventDailyStatsResponseDTO.php +++ b/backend/app/Services/Domain/Event/DTO/EventDailyStatsResponseDTO.php @@ -9,8 +9,11 @@ public function __construct( public float $total_fees, public float $total_tax, public float $total_sales_gross, - public int $tickets_sold, + public int $products_sold, public int $orders_created, + public int $attendees_registered, + public float $total_refunded, + ) { } diff --git a/backend/app/Services/Domain/Event/DuplicateEventService.php b/backend/app/Services/Domain/Event/DuplicateEventService.php index a8dd8668..cb58643f 100644 --- a/backend/app/Services/Domain/Event/DuplicateEventService.php +++ b/backend/app/Services/Domain/Event/DuplicateEventService.php @@ -13,8 +13,8 @@ use HiEvents\DomainObjects\QuestionDomainObject; use HiEvents\DomainObjects\Status\EventStatus; use HiEvents\DomainObjects\TaxAndFeesDomainObject; -use HiEvents\DomainObjects\TicketDomainObject; -use HiEvents\DomainObjects\TicketPriceDomainObject; +use HiEvents\DomainObjects\ProductDomainObject; +use HiEvents\DomainObjects\ProductPriceDomainObject; use HiEvents\Repository\Eloquent\Value\Relationship; use HiEvents\Repository\Interfaces\EventRepositoryInterface; use HiEvents\Repository\Interfaces\ImageRepositoryInterface; @@ -22,7 +22,7 @@ use HiEvents\Services\Domain\CheckInList\CreateCheckInListService; use HiEvents\Services\Domain\PromoCode\CreatePromoCodeService; use HiEvents\Services\Domain\Question\CreateQuestionService; -use HiEvents\Services\Domain\Ticket\CreateTicketService; +use HiEvents\Services\Domain\Product\CreateProductService; use HTMLPurifier; use Illuminate\Database\DatabaseManager; use Throwable; @@ -32,7 +32,7 @@ class DuplicateEventService public function __construct( private readonly EventRepositoryInterface $eventRepository, private readonly CreateEventService $createEventService, - private readonly CreateTicketService $createTicketService, + private readonly CreateProductService $createProductService, private readonly CreateQuestionService $createQuestionService, private readonly CreatePromoCodeService $createPromoCodeService, private readonly CreateCapacityAssignmentService $createCapacityAssignmentService, @@ -52,7 +52,7 @@ public function duplicateEvent( string $accountId, string $title, string $startDate, - bool $duplicateTickets = true, + bool $duplicateProducts = true, bool $duplicateQuestions = true, bool $duplicateSettings = true, bool $duplicatePromoCodes = true, @@ -84,7 +84,7 @@ public function duplicateEvent( $this->clonePerOrderQuestions($event, $newEvent->getId()); } - if ($duplicateTickets) { + if ($duplicateProducts) { $this->cloneExistingTickets( event: $event, newEventId: $newEvent->getId(), @@ -147,9 +147,9 @@ private function cloneExistingTickets( { $oldTicketToNewTicketMap = []; - foreach ($event->getTickets() as $ticket) { + foreach ($event->getProducts() as $ticket) { $ticket->setEventId($newEventId); - $newTicket = $this->createTicketService->createTicket( + $newTicket = $this->createProductService->createTicket( ticket: $ticket, accountId: $event->getAccountId(), taxAndFeeIds: $ticket->getTaxAndFees()?->map(fn($taxAndFee) => $taxAndFee->getId())?->toArray(), @@ -162,18 +162,18 @@ private function cloneExistingTickets( } if ($duplicatePromoCodes) { - $this->clonePromoCodes($event, $newEventId, $oldTicketToNewTicketMap); + $this->clonePromoCodes($event, $newEventId, $oldProductToNewProductMap); } if ($duplicateCapacityAssignments) { - $this->cloneCapacityAssignments($event, $newEventId, $oldTicketToNewTicketMap); + $this->cloneCapacityAssignments($event, $newEventId, $oldProductToNewProductMap); } if ($duplicateCheckInLists) { - $this->cloneCheckInLists($event, $newEventId, $oldTicketToNewTicketMap); + $this->cloneCheckInLists($event, $newEventId, $oldProductToNewProductMap); } - return $oldTicketToNewTicketMap; + return $oldProductToNewProductMap; } /** @@ -193,7 +193,7 @@ private function clonePerTicketQuestions(EventDomainObject $event, int $newEvent ->setOptions($question->getOptions()) ->setIsHidden($question->getIsHidden()), array_map( - static fn(TicketDomainObject $ticket) => $oldTicketToNewTicketMap[$ticket->getId()], + static fn(ProductDomainObject $ticket) => $oldTicketToNewTicketMap[$ticket->getId()], $question->getTickets()?->all(), ), ); @@ -226,16 +226,16 @@ private function clonePerOrderQuestions(EventDomainObject $event, int $newEventI /** * @throws Throwable */ - private function clonePromoCodes(EventDomainObject $event, int $newEventId, array $oldTicketToNewTicketMap): void + private function clonePromoCodes(EventDomainObject $event, int $newEventId, array $oldProductToNewProductMap): void { foreach ($event->getPromoCodes() as $promoCode) { $this->createPromoCodeService->createPromoCode( (new PromoCodeDomainObject()) ->setCode($promoCode->getCode()) ->setEventId($newEventId) - ->setApplicableTicketIds(array_map( - static fn($ticketId) => $oldTicketToNewTicketMap[$ticketId], - $promoCode->getApplicableTicketIds() ?? [], + ->setApplicableProductIds(array_map( + static fn($productId) => $oldProductToNewProductMap[$productId], + $promoCode->getApplicableProductIds() ?? [], )) ->setDiscountType($promoCode->getDiscountType()) ->setDiscount($promoCode->getDiscount()) @@ -245,7 +245,7 @@ private function clonePromoCodes(EventDomainObject $event, int $newEventId, arra } } - private function cloneCapacityAssignments(EventDomainObject $event, int $newEventId, $oldTicketToNewTicketMap): void + private function cloneCapacityAssignments(EventDomainObject $event, int $newEventId, $oldProductToNewProductMap): void { /** @var CapacityAssignmentDomainObject $capacityAssignment */ foreach ($event->getCapacityAssignments() as $capacityAssignment) { @@ -256,13 +256,13 @@ private function cloneCapacityAssignments(EventDomainObject $event, int $newEven ->setCapacity($capacityAssignment->getCapacity()) ->setAppliesTo($capacityAssignment->getAppliesTo()) ->setStatus($capacityAssignment->getStatus()), - ticketIds: $capacityAssignment->getTickets() - ?->map(fn($ticket) => $oldTicketToNewTicketMap[$ticket->getId()])?->toArray() ?? [], + productIds: $capacityAssignment->getProducts() + ?->map(fn($product) => $oldProductToNewProductMap[$product->getId()])?->toArray() ?? [], ); } } - private function cloneCheckInLists(EventDomainObject $event, int $newEventId, $oldTicketToNewTicketMap): void + private function cloneCheckInLists(EventDomainObject $event, int $newEventId, $oldProductToNewProductMap): void { foreach ($event->getCheckInLists() as $checkInList) { $this->createCheckInListService->createCheckInList( @@ -272,8 +272,8 @@ private function cloneCheckInLists(EventDomainObject $event, int $newEventId, $o ->setExpiresAt($checkInList->getExpiresAt()) ->setActivatesAt($checkInList->getActivatesAt()) ->setEventId($newEventId), - ticketIds: $checkInList->getTickets() - ?->map(fn($ticket) => $oldTicketToNewTicketMap[$ticket->getId()])?->toArray() ?? [], + productIds: $checkInList->getProducts() + ?->map(fn($product) => $oldProductToNewProductMap[$product->getId()])?->toArray() ?? [], ); } } @@ -301,20 +301,20 @@ private function getEventWithRelations(string $eventId, string $accountId): Even return $this->eventRepository ->loadRelation(EventSettingDomainObject::class) ->loadRelation( - new Relationship(TicketDomainObject::class, [ - new Relationship(TicketPriceDomainObject::class), + new Relationship(ProductDomainObject::class, [ + new Relationship(ProductPriceDomainObject::class), new Relationship(TaxAndFeesDomainObject::class) ]) ) ->loadRelation(PromoCodeDomainObject::class) ->loadRelation(new Relationship(QuestionDomainObject::class, [ - new Relationship(TicketDomainObject::class), + new Relationship(ProductDomainObject::class), ])) ->loadRelation(new Relationship(CapacityAssignmentDomainObject::class, [ - new Relationship(TicketDomainObject::class), + new Relationship(ProductDomainObject::class), ])) ->loadRelation(new Relationship(CheckInListDomainObject::class, [ - new Relationship(TicketDomainObject::class), + new Relationship(ProductDomainObject::class), ])) ->loadRelation(ImageDomainObject::class) ->findFirstWhere([ diff --git a/backend/app/Services/Domain/Event/EventStatsFetchService.php b/backend/app/Services/Domain/Event/EventStatsFetchService.php index 5344d7e4..8ec5a714 100644 --- a/backend/app/Services/Domain/Event/EventStatsFetchService.php +++ b/backend/app/Services/Domain/Event/EventStatsFetchService.php @@ -3,10 +3,10 @@ namespace HiEvents\Services\Domain\Event; use Carbon\Carbon; +use HiEvents\Services\Application\Handlers\Event\DTO\EventStatsRequestDTO; +use HiEvents\Services\Application\Handlers\Event\DTO\EventStatsResponseDTO; use HiEvents\Services\Domain\Event\DTO\EventCheckInStatsResponseDTO; use HiEvents\Services\Domain\Event\DTO\EventDailyStatsResponseDTO; -use HiEvents\Services\Handlers\Event\DTO\EventStatsRequestDTO; -use HiEvents\Services\Handlers\Event\DTO\EventStatsResponseDTO; use Illuminate\Database\DatabaseManager; use Illuminate\Support\Collection; @@ -25,12 +25,15 @@ public function getEventStats(EventStatsRequestDTO $requestData): EventStatsResp // Aggregate total statistics for the event for all time $totalsQuery = <<start_date, end_date: $requestData->end_date, check_in_stats: $this->getCheckedInStats($eventId), - total_tickets_sold: $totalsResult->total_tickets_sold ?? 0, + total_products_sold: $totalsResult->total_products_sold ?? 0, + total_attendees_registered: $totalsResult->attendees_registered ?? 0, total_orders: $totalsResult->total_orders ?? 0, total_gross_sales: $totalsResult->total_gross_sales ?? 0, total_fees: $totalsResult->total_fees ?? 0, total_tax: $totalsResult->total_tax ?? 0, total_views: $totalsResult->total_views ?? 0, - + total_refunded: $totalsResult->total_refunded ?? 0, ); } @@ -77,7 +81,9 @@ public function getDailyEventStats(EventStatsRequestDTO $requestData): Collectio COALESCE(SUM(eds.total_tax), 0) AS total_tax, COALESCE(SUM(eds.sales_total_gross), 0) AS total_sales_gross, COALESCE(SUM(eds.orders_created), 0) AS orders_created, - COALESCE(SUM(eds.tickets_sold), 0) AS tickets_sold + COALESCE(SUM(eds.products_sold), 0) AS products_sold, + COALESCE(SUM(eds.attendees_registered), 0) AS attendees_registered, + COALESCE(SUM(eds.total_refunded), 0) AS total_refunded FROM date_series ds LEFT JOIN event_daily_statistics eds ON ds.date = eds.date AND eds.deleted_at IS NULL AND eds.event_id = :eventId GROUP BY ds.date @@ -100,8 +106,10 @@ public function getDailyEventStats(EventStatsRequestDTO $requestData): Collectio total_fees: $result->total_fees, total_tax: $result->total_tax, total_sales_gross: $result->total_sales_gross, - tickets_sold: $result->tickets_sold, + products_sold: $result->products_sold, orders_created: $result->orders_created, + attendees_registered: $result->attendees_registered, + total_refunded: $result->total_refunded, ); }); } diff --git a/backend/app/Services/Domain/EventStatistics/EventStatisticsUpdateService.php b/backend/app/Services/Domain/EventStatistics/EventStatisticsUpdateService.php index 7da0c4bf..f62b1ede 100644 --- a/backend/app/Services/Domain/EventStatistics/EventStatisticsUpdateService.php +++ b/backend/app/Services/Domain/EventStatistics/EventStatisticsUpdateService.php @@ -2,16 +2,16 @@ namespace HiEvents\Services\Domain\EventStatistics; +use HiEvents\DomainObjects\Generated\ProductDomainObjectAbstract; use HiEvents\DomainObjects\Generated\PromoCodeDomainObjectAbstract; -use HiEvents\DomainObjects\Generated\TicketDomainObjectAbstract; use HiEvents\DomainObjects\OrderDomainObject; use HiEvents\DomainObjects\OrderItemDomainObject; use HiEvents\Exceptions\EventStatisticsVersionMismatchException; use HiEvents\Repository\Interfaces\EventDailyStatisticRepositoryInterface; use HiEvents\Repository\Interfaces\EventStatisticRepositoryInterface; use HiEvents\Repository\Interfaces\OrderRepositoryInterface; +use HiEvents\Repository\Interfaces\ProductRepositoryInterface; use HiEvents\Repository\Interfaces\PromoCodeRepositoryInterface; -use HiEvents\Repository\Interfaces\TicketRepositoryInterface; use HiEvents\Values\MoneyValue; use Illuminate\Database\DatabaseManager; use Illuminate\Support\Carbon; @@ -26,7 +26,7 @@ { public function __construct( private PromoCodeRepositoryInterface $promoCodeRepository, - private TicketRepositoryInterface $ticketRepository, + private ProductRepositoryInterface $productRepository, private EventStatisticRepositoryInterface $eventStatisticsRepository, private EventDailyStatisticRepositoryInterface $eventDailyStatisticRepository, private DatabaseManager $databaseManager, @@ -50,7 +50,7 @@ public function updateStatistics(OrderDomainObject $order): void $this->updateEventStats($order); $this->updateEventDailyStats($order); $this->updatePromoCodeCounts($order); - $this->updateTicketStatistics($order); + $this->updateProductStatistics($order); }); } @@ -126,12 +126,12 @@ private function updatePromoCodeCounts(OrderDomainObject $order): void } } - private function updateTicketStatistics(OrderDomainObject $order): void + private function updateProductStatistics(OrderDomainObject $order): void { foreach ($order->getOrderItems() as $orderItem) { - $this->ticketRepository->increment( - $orderItem->getTicketId(), - TicketDomainObjectAbstract::SALES_VOLUME, + $this->productRepository->increment( + $orderItem->getProductId(), + ProductDomainObjectAbstract::SALES_VOLUME, $orderItem->getTotalBeforeAdditions(), ); } @@ -153,7 +153,9 @@ private function updateEventStats(OrderDomainObject $order): void if ($eventStatistics === null) { $this->eventStatisticsRepository->create([ 'event_id' => $order->getEventId(), - 'tickets_sold' => $order->getOrderItems() + 'products_sold' => $order->getOrderItems() + ?->sum(fn(OrderItemDomainObject $orderItem) => $orderItem->getQuantity()), + 'attendees_registered' => $order->getTicketOrderItems() ?->sum(fn(OrderItemDomainObject $orderItem) => $orderItem->getQuantity()), 'sales_total_gross' => $order->getTotalGross(), 'sales_total_before_additions' => $order->getTotalBeforeAdditions(), @@ -167,7 +169,9 @@ private function updateEventStats(OrderDomainObject $order): void $update = $this->eventStatisticsRepository->updateWhere( attributes: [ - 'tickets_sold' => $eventStatistics->getTicketsSold() + $order->getOrderItems() + 'products_sold' => $eventStatistics->getProductsSold() + $order->getOrderItems() + ?->sum(fn(OrderItemDomainObject $orderItem) => $orderItem->getQuantity()), + 'attendees_registered' => $eventStatistics->getAttendeesRegistered() + $order->getTicketOrderItems() ?->sum(fn(OrderItemDomainObject $orderItem) => $orderItem->getQuantity()), 'sales_total_gross' => $eventStatistics->getSalesTotalGross() + $order->getTotalGross(), 'sales_total_before_additions' => $eventStatistics->getSalesTotalBeforeAdditions() + $order->getTotalBeforeAdditions(), @@ -208,7 +212,8 @@ private function updateEventDailyStats(OrderDomainObject $order): void $this->eventDailyStatisticRepository->create([ 'event_id' => $order->getEventId(), 'date' => (new Carbon($order->getCreatedAt()))->format('Y-m-d'), - 'tickets_sold' => $order->getOrderItems()?->sum(fn(OrderItemDomainObject $orderItem) => $orderItem->getQuantity()), + 'products_sold' => $order->getOrderItems()?->sum(fn(OrderItemDomainObject $orderItem) => $orderItem->getQuantity()), + 'attendees_registered' => $order->getTicketOrderItems()?->sum(fn(OrderItemDomainObject $orderItem) => $orderItem->getQuantity()), 'sales_total_gross' => $order->getTotalGross(), 'sales_total_before_additions' => $order->getTotalBeforeAdditions(), 'total_tax' => $order->getTotalTax(), @@ -220,7 +225,8 @@ private function updateEventDailyStats(OrderDomainObject $order): void $update = $this->eventDailyStatisticRepository->updateWhere( attributes: [ - 'tickets_sold' => $eventDailyStatistic->getTicketsSold() + $order->getOrderItems()->sum(fn(OrderItemDomainObject $orderItem) => $orderItem->getQuantity()), + 'attendees_registered' => $eventDailyStatistic->getAttendeesRegistered() + $order->getTicketOrderItems()->sum(fn(OrderItemDomainObject $orderItem) => $orderItem->getQuantity()), + 'products_sold' => $eventDailyStatistic->getProductsSold() + $order->getOrderItems()->sum(fn(OrderItemDomainObject $orderItem) => $orderItem->getQuantity()), 'sales_total_gross' => $eventDailyStatistic->getSalesTotalGross() + $order->getTotalGross(), 'sales_total_before_additions' => $eventDailyStatistic->getSalesTotalBeforeAdditions() + $order->getTotalBeforeAdditions(), 'total_tax' => $eventDailyStatistic->getTotalTax() + $order->getTotalTax(), diff --git a/backend/app/Services/Domain/Mail/SendEventEmailMessagesService.php b/backend/app/Services/Domain/Mail/SendEventEmailMessagesService.php index f4a6b05c..88888252 100644 --- a/backend/app/Services/Domain/Mail/SendEventEmailMessagesService.php +++ b/backend/app/Services/Domain/Mail/SendEventEmailMessagesService.php @@ -18,7 +18,7 @@ use HiEvents\Repository\Interfaces\MessageRepositoryInterface; use HiEvents\Repository\Interfaces\OrderRepositoryInterface; use HiEvents\Repository\Interfaces\UserRepositoryInterface; -use HiEvents\Services\Handlers\Message\DTO\SendMessageDTO; +use HiEvents\Services\Application\Handlers\Message\DTO\SendMessageDTO; use Illuminate\Mail\Mailer; use Illuminate\Support\Collection; use Symfony\Component\HttpKernel\Log\Logger; @@ -70,8 +70,8 @@ public function send(SendMessageDTO $messageData): void case MessageTypeEnum::ORDER: $this->sendOrderMessages($messageData, $event, $order); break; - case MessageTypeEnum::TICKET: - $this->sendTicketMessages($messageData, $event); + case MessageTypeEnum::PRODUCT: + $this->sendProductMessages($messageData, $event); break; case MessageTypeEnum::EVENT: $this->sendEventMessages($messageData, $event); @@ -95,11 +95,11 @@ private function sendAttendeeMessages(SendMessageDTO $messageData, EventDomainOb $this->emailAttendees($attendees, $messageData, $event); } - private function sendTicketMessages(SendMessageDTO $messageData, EventDomainObject $event): void + private function sendProductMessages(SendMessageDTO $messageData, EventDomainObject $event): void { $attendees = $this->attendeeRepository->findWhereIn( - field: 'ticket_id', - values: $messageData->ticket_ids, + field: 'product_id', + values: $messageData->product_ids, additionalWhere: [ 'event_id' => $messageData->event_id, 'status' => AttendeeStatus::ACTIVE->name, diff --git a/backend/app/Services/Domain/Mail/SendOrderDetailsService.php b/backend/app/Services/Domain/Mail/SendOrderDetailsService.php index 62040573..acf9caf9 100644 --- a/backend/app/Services/Domain/Mail/SendOrderDetailsService.php +++ b/backend/app/Services/Domain/Mail/SendOrderDetailsService.php @@ -23,12 +23,12 @@ public function __construct( private EventRepositoryInterface $eventRepository, private OrderRepositoryInterface $orderRepository, private Mailer $mailer, - private SendAttendeeTicketService $sendAttendeeTicketService, + private SendAttendeeTicketService $sendAttendeeProductService, ) { } - public function sendOrderSummaryAndTicketEmails(OrderDomainObject $order): void + public function sendOrderSummaryAndProductEmails(OrderDomainObject $order): void { $order = $this->orderRepository ->loadRelation(OrderItemDomainObject::class) @@ -42,7 +42,7 @@ public function sendOrderSummaryAndTicketEmails(OrderDomainObject $order): void if ($order->isOrderCompleted()) { $this->sendOrderSummaryEmails($order, $event); - $this->sendAttendeeTicketEmails($order, $event); + $this->sendAttendeeProductEmails($order, $event); } if ($order->isOrderFailed()) { @@ -57,7 +57,7 @@ public function sendOrderSummaryAndTicketEmails(OrderDomainObject $order): void } } - private function sendAttendeeTicketEmails(OrderDomainObject $order, EventDomainObject $event): void + private function sendAttendeeProductEmails(OrderDomainObject $order, EventDomainObject $event): void { $sentEmails = []; foreach ($order->getAttendees() as $attendee) { @@ -65,7 +65,7 @@ private function sendAttendeeTicketEmails(OrderDomainObject $order, EventDomainO continue; } - $this->sendAttendeeTicketService->send( + $this->sendAttendeeProductService->send( attendee: $attendee, event: $event, eventSettings: $event->getEventSettings(), diff --git a/backend/app/Services/Domain/Order/OrderCancelService.php b/backend/app/Services/Domain/Order/OrderCancelService.php index e439f26d..c82a3672 100644 --- a/backend/app/Services/Domain/Order/OrderCancelService.php +++ b/backend/app/Services/Domain/Order/OrderCancelService.php @@ -11,7 +11,7 @@ use HiEvents\Repository\Interfaces\AttendeeRepositoryInterface; use HiEvents\Repository\Interfaces\EventRepositoryInterface; use HiEvents\Repository\Interfaces\OrderRepositoryInterface; -use HiEvents\Services\Domain\Ticket\TicketQuantityUpdateService; +use HiEvents\Services\Domain\Product\ProductQuantityUpdateService; use Illuminate\Contracts\Mail\Mailer; use Illuminate\Database\DatabaseManager; use Throwable; @@ -19,12 +19,12 @@ readonly class OrderCancelService { public function __construct( - private Mailer $mailer, - private AttendeeRepositoryInterface $attendeeRepository, - private EventRepositoryInterface $eventRepository, - private OrderRepositoryInterface $orderRepository, - private DatabaseManager $databaseManager, - private TicketQuantityUpdateService $ticketQuantityService, + private Mailer $mailer, + private AttendeeRepositoryInterface $attendeeRepository, + private EventRepositoryInterface $eventRepository, + private OrderRepositoryInterface $orderRepository, + private DatabaseManager $databaseManager, + private ProductQuantityUpdateService $productQuantityService, ) { } @@ -35,7 +35,7 @@ public function __construct( public function cancelOrder(OrderDomainObject $order): void { $this->databaseManager->transaction(function () use ($order) { - $this->adjustTicketQuantities($order); + $this->adjustProductQuantities($order); $this->cancelAttendees($order); $this->updateOrderStatus($order); @@ -66,18 +66,18 @@ private function cancelAttendees(OrderDomainObject $order): void ); } - private function adjustTicketQuantities(OrderDomainObject $order): void + private function adjustProductQuantities(OrderDomainObject $order): void { $attendees = $this->attendeeRepository->findWhere([ 'order_id' => $order->getId(), 'status' => AttendeeStatus::ACTIVE->name, ]); - $ticketIdCountMap = $attendees - ->map(fn(AttendeeDomainObject $attendee) => $attendee->getTicketPriceId())->countBy(); + $productIdCountMap = $attendees + ->map(fn(AttendeeDomainObject $attendee) => $attendee->getProductPriceId())->countBy(); - foreach ($ticketIdCountMap as $ticketPriceId => $count) { - $this->ticketQuantityService->decreaseQuantitySold($ticketPriceId, $count); + foreach ($productIdCountMap as $productPriceId => $count) { + $this->productQuantityService->decreaseQuantitySold($productPriceId, $count); } } diff --git a/backend/app/Services/Domain/Order/OrderCreateRequestValidationService.php b/backend/app/Services/Domain/Order/OrderCreateRequestValidationService.php index b55ad8c7..f075f65b 100644 --- a/backend/app/Services/Domain/Order/OrderCreateRequestValidationService.php +++ b/backend/app/Services/Domain/Order/OrderCreateRequestValidationService.php @@ -4,18 +4,18 @@ use Exception; use HiEvents\DomainObjects\CapacityAssignmentDomainObject; -use HiEvents\DomainObjects\Enums\TicketType; +use HiEvents\DomainObjects\Enums\ProductPriceType; use HiEvents\DomainObjects\EventDomainObject; use HiEvents\DomainObjects\Generated\PromoCodeDomainObjectAbstract; -use HiEvents\DomainObjects\TicketDomainObject; -use HiEvents\DomainObjects\TicketPriceDomainObject; +use HiEvents\DomainObjects\ProductDomainObject; +use HiEvents\DomainObjects\ProductPriceDomainObject; use HiEvents\Helper\Currency; use HiEvents\Repository\Interfaces\EventRepositoryInterface; use HiEvents\Repository\Interfaces\PromoCodeRepositoryInterface; -use HiEvents\Repository\Interfaces\TicketRepositoryInterface; -use HiEvents\Services\Domain\Ticket\AvailableTicketQuantitiesFetchService; -use HiEvents\Services\Domain\Ticket\DTO\AvailableTicketQuantitiesDTO; -use HiEvents\Services\Domain\Ticket\DTO\AvailableTicketQuantitiesResponseDTO; +use HiEvents\Repository\Interfaces\ProductRepositoryInterface; +use HiEvents\Services\Domain\Product\AvailableProductQuantitiesFetchService; +use HiEvents\Services\Domain\Product\DTO\AvailableProductQuantitiesDTO; +use HiEvents\Services\Domain\Product\DTO\AvailableProductQuantitiesResponseDTO; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\ValidationException; @@ -23,13 +23,13 @@ class OrderCreateRequestValidationService { - private AvailableTicketQuantitiesResponseDTO $availableTicketQuantities; + private AvailableProductQuantitiesResponseDTO $availableProductQuantities; public function __construct( - readonly private TicketRepositoryInterface $ticketRepository, - readonly private PromoCodeRepositoryInterface $promoCodeRepository, - readonly private EventRepositoryInterface $eventRepository, - readonly private AvailableTicketQuantitiesFetchService $fetchAvailableTicketQuantitiesService, + readonly private ProductRepositoryInterface $productRepository, + readonly private PromoCodeRepositoryInterface $promoCodeRepository, + readonly private EventRepositoryInterface $eventRepository, + readonly private AvailableProductQuantitiesFetchService $fetchAvailableProductQuantitiesService, ) { } @@ -44,16 +44,16 @@ public function validateRequestData(int $eventId, array $data = []): void $event = $this->eventRepository->findById($eventId); $this->validatePromoCode($eventId, $data); - $this->validateTicketSelection($data); + $this->validateProductSelection($data); - $this->availableTicketQuantities = $this->fetchAvailableTicketQuantitiesService - ->getAvailableTicketQuantities( + $this->availableProductQuantities = $this->fetchAvailableProductQuantitiesService + ->getAvailableProductQuantities( $event->getId(), ignoreCache: true, ); $this->validateOverallCapacity($data); - $this->validateTicketDetails($event, $data); + $this->validateProductDetails($event, $data); } /** @@ -81,12 +81,12 @@ private function validatePromoCode(int $eventId, array $data): void private function validateTypes(array $data): void { $validator = Validator::make($data, [ - 'tickets' => 'required|array', - 'tickets.*.ticket_id' => 'required|integer', - 'tickets.*.quantities' => 'required|array', - 'tickets.*.quantities.*.quantity' => 'required|integer', - 'tickets.*.quantities.*.price_id' => 'required|integer', - 'tickets.*.quantities.*.price' => 'numeric|min:0', + 'products' => 'required|array', + 'products.*.product_id' => 'required|integer', + 'products.*.quantities' => 'required|array', + 'products.*.quantities.*.quantity' => 'required|integer', + 'products.*.quantities.*.price_id' => 'required|integer', + 'products.*.quantities.*.price' => 'numeric|min:0', ]); if ($validator->fails()) { @@ -97,12 +97,12 @@ private function validateTypes(array $data): void /** * @throws ValidationException */ - private function validateTicketSelection(array $data): void + private function validateProductSelection(array $data): void { - $ticketData = collect($data['tickets']); - if ($ticketData->isEmpty() || $ticketData->sum(fn($ticket) => collect($ticket['quantities'])->sum('quantity')) === 0) { + $productData = collect($data['products']); + if ($productData->isEmpty() || $productData->sum(fn($product) => collect($product['quantities'])->sum('quantity')) === 0) { throw ValidationException::withMessages([ - 'tickets' => __('You haven\'t selected any tickets') + 'products' => __('You haven\'t selected any products') ]); } } @@ -110,150 +110,150 @@ private function validateTicketSelection(array $data): void /** * @throws Exception */ - private function getTickets(array $data): Collection + private function getProducts(array $data): Collection { - $ticketIds = collect($data['tickets'])->pluck('ticket_id'); - return $this->ticketRepository - ->loadRelation(TicketPriceDomainObject::class) - ->findWhereIn('id', $ticketIds->toArray()); + $productIds = collect($data['products'])->pluck('product_id'); + return $this->productRepository + ->loadRelation(ProductPriceDomainObject::class) + ->findWhereIn('id', $productIds->toArray()); } /** * @throws ValidationException * @throws Exception */ - private function validateTicketDetails(EventDomainObject $event, array $data): void + private function validateProductDetails(EventDomainObject $event, array $data): void { - $tickets = $this->getTickets($data); + $products = $this->getProducts($data); - foreach ($data['tickets'] as $ticketIndex => $ticketAndQuantities) { - $this->validateSingleTicketDetails($event, $ticketIndex, $ticketAndQuantities, $tickets); + foreach ($data['products'] as $productIndex => $productAndQuantities) { + $this->validateSingleProductDetails($event, $productIndex, $productAndQuantities, $products); } } /** * @throws ValidationException */ - private function validateSingleTicketDetails(EventDomainObject $event, int $ticketIndex, array $ticketAndQuantities, $tickets): void + private function validateSingleProductDetails(EventDomainObject $event, int $productIndex, array $productAndQuantities, $products): void { - $ticketId = $ticketAndQuantities['ticket_id']; - $totalQuantity = collect($ticketAndQuantities['quantities'])->sum('quantity'); + $productId = $productAndQuantities['product_id']; + $totalQuantity = collect($productAndQuantities['quantities'])->sum('quantity'); if ($totalQuantity === 0) { return; } - /** @var TicketDomainObject $ticket */ - $ticket = $tickets->filter(fn($t) => $t->getId() === $ticketId)->first(); - if (!$ticket) { - throw new NotFoundHttpException(sprintf('Ticket ID %d not found', $ticketId)); + /** @var ProductDomainObject $product */ + $product = $products->filter(fn($t) => $t->getId() === $productId)->first(); + if (!$product) { + throw new NotFoundHttpException(sprintf('Product ID %d not found', $productId)); } - $this->validateTicketEvent( + $this->validateProductEvent( event: $event, - ticketId: $ticketId, - ticket: $ticket + productId: $productId, + product: $product ); - $this->validateTicketQuantity( - ticketIndex: $ticketIndex, - ticketAndQuantities: $ticketAndQuantities, - ticket: $ticket + $this->validateProductQuantity( + productIndex: $productIndex, + productAndQuantities: $productAndQuantities, + product: $product ); - $this->validateTicketTypeAndPrice( + $this->validateProductTypeAndPrice( event: $event, - ticketIndex: $ticketIndex, - ticketAndQuantities: $ticketAndQuantities, - ticket: $ticket + productIndex: $productIndex, + productAndQuantities: $productAndQuantities, + product: $product ); - $this->validateSoldOutTickets( - ticketId: $ticketId, - ticketIndex: $ticketIndex, - ticket: $ticket + $this->validateSoldOutProducts( + productId: $productId, + productIndex: $productIndex, + product: $product ); $this->validatePriceIdAndQuantity( - ticketIndex: $ticketIndex, - ticketAndQuantities: $ticketAndQuantities, - ticket: $ticket + productIndex: $productIndex, + productAndQuantities: $productAndQuantities, + product: $product ); } /** * @throws ValidationException */ - private function validateTicketQuantity(int $ticketIndex, array $ticketAndQuantities, TicketDomainObject $ticket): void + private function validateProductQuantity(int $productIndex, array $productAndQuantities, ProductDomainObject $product): void { - $totalQuantity = collect($ticketAndQuantities['quantities'])->sum('quantity'); - $maxPerOrder = (int)$ticket->getMaxPerOrder() ?: 100; + $totalQuantity = collect($productAndQuantities['quantities'])->sum('quantity'); + $maxPerOrder = (int)$product->getMaxPerOrder() ?: 100; - $capacityMaximum = $this->availableTicketQuantities - ->ticketQuantities - ->where('ticket_id', $ticket->getId()) - ->map(fn(AvailableTicketQuantitiesDTO $price) => $price->capacities) + $capacityMaximum = $this->availableProductQuantities + ->productQuantities + ->where('product_id', $product->getId()) + ->map(fn(AvailableProductQuantitiesDTO $price) => $price->capacities) ->flatten() ->min(fn(CapacityAssignmentDomainObject $capacity) => $capacity->getCapacity()); - $ticketAvailableQuantity = $this->availableTicketQuantities - ->ticketQuantities - ->first(fn(AvailableTicketQuantitiesDTO $price) => $price->ticket_id === $ticket->getId()) + $productAvailableQuantity = $this->availableProductQuantities + ->productQuantities + ->first(fn(AvailableProductQuantitiesDTO $price) => $price->product_id === $product->getId()) ->quantity_available; - # if there are fewer tickets available than the configured minimum, we allow less than the minimum to be purchased - $minPerOrder = min((int)$ticket->getMinPerOrder() ?: 1, + # if there are fewer products available than the configured minimum, we allow less than the minimum to be purchased + $minPerOrder = min((int)$product->getMinPerOrder() ?: 1, $capacityMaximum ?: $maxPerOrder, - $ticketAvailableQuantity ?: $maxPerOrder); + $productAvailableQuantity ?: $maxPerOrder); - $this->validateTicketPricesQuantity( - quantities: $ticketAndQuantities['quantities'], - ticket: $ticket, - ticketIndex: $ticketIndex + $this->validateProductPricesQuantity( + quantities: $productAndQuantities['quantities'], + product: $product, + productIndex: $productIndex ); if ($totalQuantity > $maxPerOrder) { throw ValidationException::withMessages([ - "tickets.$ticketIndex" => __("The maximum number of tickets available for :tickets is :max", [ + "products.$productIndex" => __("The maximum number of products available for :products is :max", [ 'max' => $maxPerOrder, - 'ticket' => $ticket->getTitle(), + 'product' => $product->getTitle(), ]), ]); } if ($totalQuantity < $minPerOrder) { throw ValidationException::withMessages([ - "tickets.$ticketIndex" => __("You must order at least :min tickets for :ticket", [ + "products.$productIndex" => __("You must order at least :min products for :product", [ 'min' => $minPerOrder, - 'ticket' => $ticket->getTitle(), + 'product' => $product->getTitle(), ]), ]); } } - private function validateTicketEvent(EventDomainObject $event, int $ticketId, TicketDomainObject $ticket): void + private function validateProductEvent(EventDomainObject $event, int $productId, ProductDomainObject $product): void { - if ($ticket->getEventId() !== $event->getId()) { - throw new NotFoundHttpException(sprintf('Ticket ID %d not found for event ID %d', $ticketId, $event->getId())); + if ($product->getEventId() !== $event->getId()) { + throw new NotFoundHttpException(sprintf('Product ID %d not found for event ID %d', $productId, $event->getId())); } } /** * @throws ValidationException */ - private function validateTicketTypeAndPrice( + private function validateProductTypeAndPrice( EventDomainObject $event, - int $ticketIndex, - array $ticketAndQuantities, - TicketDomainObject $ticket + int $productIndex, + array $productAndQuantities, + ProductDomainObject $product ): void { - if ($ticket->getType() === TicketType::DONATION->name) { - $price = $ticketAndQuantities['quantities'][0]['price'] ?? 0; - if ($price < $ticket->getPrice()) { - $formattedPrice = Currency::format($ticket->getPrice(), $event->getCurrency()); + if ($product->getType() === ProductPriceType::DONATION->name) { + $price = $productAndQuantities['quantities'][0]['price'] ?? 0; + if ($price < $product->getPrice()) { + $formattedPrice = Currency::format($product->getPrice(), $event->getCurrency()); throw ValidationException::withMessages([ - "tickets.$ticketIndex.quantities.0.price" => __("The minimum amount is :price", ['price' => $formattedPrice]), + "products.$productIndex.quantities.0.price" => __("The minimum amount is :price", ['price' => $formattedPrice]), ]); } } @@ -262,13 +262,13 @@ private function validateTicketTypeAndPrice( /** * @throws ValidationException */ - private function validateSoldOutTickets(int $ticketId, int $ticketIndex, TicketDomainObject $ticket): void + private function validateSoldOutProducts(int $productId, int $productIndex, ProductDomainObject $product): void { - if ($ticket->isSoldOut()) { + if ($product->isSoldOut()) { throw ValidationException::withMessages([ - "tickets.$ticketIndex" => __("The ticket :ticket is sold out", [ - 'id' => $ticketId, - 'ticket' => $ticket->getTitle(), + "products.$productIndex" => __("The product :product is sold out", [ + 'id' => $productId, + 'product' => $product->getTitle(), ]), ]); } @@ -277,24 +277,24 @@ private function validateSoldOutTickets(int $ticketId, int $ticketIndex, TicketD /** * @throws ValidationException */ - private function validatePriceIdAndQuantity(int $ticketIndex, array $ticketAndQuantities, TicketDomainObject $ticket): void + private function validatePriceIdAndQuantity(int $productIndex, array $productAndQuantities, ProductDomainObject $product): void { $errors = []; - foreach ($ticketAndQuantities['quantities'] as $quantityIndex => $quantityData) { + foreach ($productAndQuantities['quantities'] as $quantityIndex => $quantityData) { $priceId = $quantityData['price_id'] ?? null; $quantity = $quantityData['quantity'] ?? null; if (null === $priceId || null === $quantity) { $missingField = null === $priceId ? 'price_id' : 'quantity'; - $errors["tickets.$ticketIndex.quantities.$quantityIndex.$missingField"] = __(":field must be specified", [ + $errors["products.$productIndex.quantities.$quantityIndex.$missingField"] = __(":field must be specified", [ 'field' => ucfirst($missingField) ]); } - $validPriceIds = $ticket->getTicketPrices()?->map(fn(TicketPriceDomainObject $price) => $price->getId()); + $validPriceIds = $product->getProductPrices()?->map(fn(ProductPriceDomainObject $price) => $price->getId()); if (!in_array($priceId, $validPriceIds->toArray(), true)) { - $errors["tickets.$ticketIndex.quantities.$quantityIndex.price_id"] = __('Invalid price ID'); + $errors["products.$productIndex.quantities.$quantityIndex.price_id"] = __('Invalid price ID'); } } @@ -306,32 +306,32 @@ private function validatePriceIdAndQuantity(int $ticketIndex, array $ticketAndQu /** * @throws ValidationException */ - private function validateTicketPricesQuantity(array $quantities, TicketDomainObject $ticket, int $ticketIndex): void + private function validateProductPricesQuantity(array $quantities, ProductDomainObject $product, int $productIndex): void { - foreach ($quantities as $ticketQuantity) { - $numberAvailable = $this->availableTicketQuantities - ->ticketQuantities - ->where('ticket_id', $ticket->getId()) - ->where('price_id', $ticketQuantity['price_id']) + foreach ($quantities as $productQuantity) { + $numberAvailable = $this->availableProductQuantities + ->productQuantities + ->where('product_id', $product->getId()) + ->where('price_id', $productQuantity['price_id']) ->first()?->quantity_available; - /** @var TicketPriceDomainObject $ticketPrice */ - $ticketPrice = $ticket->getTicketPrices() - ?->first(fn(TicketPriceDomainObject $price) => $price->getId() === $ticketQuantity['price_id']); + /** @var ProductPriceDomainObject $productPrice */ + $productPrice = $product->getProductPrices() + ?->first(fn(ProductPriceDomainObject $price) => $price->getId() === $productQuantity['price_id']); - if ($ticketQuantity['quantity'] > $numberAvailable) { + if ($productQuantity['quantity'] > $numberAvailable) { if ($numberAvailable === 0) { throw ValidationException::withMessages([ - "tickets.$ticketIndex" => __("The ticket :ticket is sold out", [ - 'ticket' => $ticket->getTitle() . ($ticketPrice->getLabel() ? ' - ' . $ticketPrice->getLabel() : ''), + "products.$productIndex" => __("The product :product is sold out", [ + 'product' => $product->getTitle() . ($productPrice->getLabel() ? ' - ' . $productPrice->getLabel() : ''), ]), ]); } throw ValidationException::withMessages([ - "tickets.$ticketIndex" => __("The maximum number of tickets available for :ticket is :max", [ + "products.$productIndex" => __("The maximum number of products available for :product is :max", [ 'max' => $numberAvailable, - 'ticket' => $ticket->getTitle() . ($ticketPrice->getLabel() ? ' - ' . $ticketPrice->getLabel() : ''), + 'product' => $product->getTitle() . ($productPrice->getLabel() ? ' - ' . $productPrice->getLabel() : ''), ]), ]); } @@ -343,34 +343,34 @@ private function validateTicketPricesQuantity(array $quantities, TicketDomainObj */ private function validateOverallCapacity(array $data): void { - foreach ($this->availableTicketQuantities->capacities as $capacity) { - if ($capacity->getTickets() === null) { + foreach ($this->availableProductQuantities->capacities as $capacity) { + if ($capacity->getProducts() === null) { continue; } - $ticketIds = $capacity->getTickets()->map(fn(TicketDomainObject $ticket) => $ticket->getId()); - $totalQuantity = collect($data['tickets']) - ->filter(fn($ticket) => in_array($ticket['ticket_id'], $ticketIds->toArray(), true)) - ->sum(fn($ticket) => collect($ticket['quantities'])->sum('quantity')); + $productIds = $capacity->getProducts()->map(fn(ProductDomainObject $product) => $product->getId()); + $totalQuantity = collect($data['products']) + ->filter(fn($product) => in_array($product['product_id'], $productIds->toArray(), true)) + ->sum(fn($product) => collect($product['quantities'])->sum('quantity')); - $reservedTicketQuantities = $capacity->getTickets() - ->map(fn(TicketDomainObject $ticket) => $this - ->availableTicketQuantities - ->ticketQuantities - ->where('ticket_id', $ticket->getId()) + $reservedProductQuantities = $capacity->getProducts() + ->map(fn(ProductDomainObject $product) => $this + ->availableProductQuantities + ->productQuantities + ->where('product_id', $product->getId()) ->sum('quantity_reserved') ) ->sum(); - if ($totalQuantity > ($capacity->getAvailableCapacity() - $reservedTicketQuantities)) { - if ($capacity->getAvailableCapacity() - $reservedTicketQuantities <= 0) { + if ($totalQuantity > ($capacity->getAvailableCapacity() - $reservedProductQuantities)) { + if ($capacity->getAvailableCapacity() - $reservedProductQuantities <= 0) { throw ValidationException::withMessages([ - 'tickets' => __('Sorry, these tickets are sold out'), + 'products' => __('Sorry, these products are sold out'), ]); } throw ValidationException::withMessages([ - 'tickets' => __('The maximum number of tickets available is :max', [ + 'products' => __('The maximum number of products available is :max', [ 'max' => $capacity->getAvailableCapacity(), ]), ]); diff --git a/backend/app/Services/Domain/Order/OrderItemProcessingService.php b/backend/app/Services/Domain/Order/OrderItemProcessingService.php index 3ab93a6e..f0b784b5 100644 --- a/backend/app/Services/Domain/Order/OrderItemProcessingService.php +++ b/backend/app/Services/Domain/Order/OrderItemProcessingService.php @@ -3,19 +3,19 @@ namespace HiEvents\Services\Domain\Order; use HiEvents\DomainObjects\EventDomainObject; -use HiEvents\DomainObjects\Generated\TicketDomainObjectAbstract; +use HiEvents\DomainObjects\Generated\ProductDomainObjectAbstract; use HiEvents\DomainObjects\OrderDomainObject; +use HiEvents\DomainObjects\ProductDomainObject; +use HiEvents\DomainObjects\ProductPriceDomainObject; use HiEvents\DomainObjects\PromoCodeDomainObject; use HiEvents\DomainObjects\TaxAndFeesDomainObject; -use HiEvents\DomainObjects\TicketDomainObject; -use HiEvents\DomainObjects\TicketPriceDomainObject; use HiEvents\Helper\Currency; use HiEvents\Repository\Interfaces\OrderRepositoryInterface; -use HiEvents\Repository\Interfaces\TicketRepositoryInterface; +use HiEvents\Repository\Interfaces\ProductRepositoryInterface; +use HiEvents\Services\Application\Handlers\Order\DTO\ProductOrderDetailsDTO; +use HiEvents\Services\Domain\Product\DTO\OrderProductPriceDTO; +use HiEvents\Services\Domain\Product\ProductPriceService; use HiEvents\Services\Domain\Tax\TaxAndFeeCalculationService; -use HiEvents\Services\Domain\Ticket\DTO\OrderTicketPriceDTO; -use HiEvents\Services\Domain\Ticket\TicketPriceService; -use HiEvents\Services\Handlers\Order\DTO\TicketOrderDetailsDTO; use Illuminate\Support\Collection; use Symfony\Component\Routing\Exception\ResourceNotFoundException; @@ -23,49 +23,49 @@ { public function __construct( private OrderRepositoryInterface $orderRepository, - private TicketRepositoryInterface $ticketRepository, + private ProductRepositoryInterface $productRepository, private TaxAndFeeCalculationService $taxCalculationService, - private TicketPriceService $ticketPriceService, + private ProductPriceService $productPriceService, ) { } /** * @param OrderDomainObject $order - * @param Collection $ticketsOrderDetails + * @param Collection $productsOrderDetails * @param EventDomainObject $event * @param PromoCodeDomainObject|null $promoCode * @return Collection */ public function process( OrderDomainObject $order, - Collection $ticketsOrderDetails, + Collection $productsOrderDetails, EventDomainObject $event, ?PromoCodeDomainObject $promoCode ): Collection { $orderItems = collect(); - foreach ($ticketsOrderDetails as $ticketOrderDetail) { - $ticket = $this->ticketRepository + foreach ($productsOrderDetails as $productOrderDetail) { + $product = $this->productRepository ->loadRelation(TaxAndFeesDomainObject::class) - ->loadRelation(TicketPriceDomainObject::class) + ->loadRelation(ProductPriceDomainObject::class) ->findFirstWhere([ - TicketDomainObjectAbstract::ID => $ticketOrderDetail->ticket_id, - TicketDomainObjectAbstract::EVENT_ID => $event->getId(), + ProductDomainObjectAbstract::ID => $productOrderDetail->product_id, + ProductDomainObjectAbstract::EVENT_ID => $event->getId(), ]); - if ($ticket === null) { + if ($product === null) { throw new ResourceNotFoundException( - __('Ticket with id :id not found', ['id' => $ticketOrderDetail->ticket_id]) + __('Product with id :id not found', ['id' => $productOrderDetail->product_id]) ); } - $ticketOrderDetail->quantities->each(function (OrderTicketPriceDTO $ticketPrice) use ($promoCode, $order, $orderItems, $ticket) { - if ($ticketPrice->quantity === 0) { + $productOrderDetail->quantities->each(function (OrderProductPriceDTO $productPrice) use ($promoCode, $order, $orderItems, $product) { + if ($productPrice->quantity === 0) { return; } - $orderItemData = $this->calculateOrderItemData($ticket, $ticketPrice, $order, $promoCode); + $orderItemData = $this->calculateOrderItemData($product, $productPrice, $order, $promoCode); $orderItems->push($this->orderRepository->addOrderItem($orderItemData)); }); } @@ -74,33 +74,34 @@ public function process( } private function calculateOrderItemData( - TicketDomainObject $ticket, - OrderTicketPriceDTO $ticketPriceDetails, + ProductDomainObject $product, + OrderProductPriceDTO $productPriceDetails, OrderDomainObject $order, ?PromoCodeDomainObject $promoCode ): array { - $prices = $this->ticketPriceService->getPrice($ticket, $ticketPriceDetails, $promoCode); + $prices = $this->productPriceService->getPrice($product, $productPriceDetails, $promoCode); $priceWithDiscount = $prices->price; $priceBeforeDiscount = $prices->price_before_discount; - $itemTotalWithDiscount = $priceWithDiscount * $ticketPriceDetails->quantity; + $itemTotalWithDiscount = $priceWithDiscount * $productPriceDetails->quantity; - $taxesAndFees = $this->taxCalculationService->calculateTaxAndFeesForTicket( - ticket: $ticket, + $taxesAndFees = $this->taxCalculationService->calculateTaxAndFeesForProduct( + product: $product, price: $priceWithDiscount, - quantity: $ticketPriceDetails->quantity + quantity: $productPriceDetails->quantity ); return [ - 'ticket_id' => $ticket->getId(), - 'ticket_price_id' => $ticketPriceDetails->price_id, - 'quantity' => $ticketPriceDetails->quantity, + 'product_type' => $product->getProductType(), + 'product_id' => $product->getId(), + 'product_price_id' => $productPriceDetails->price_id, + 'quantity' => $productPriceDetails->quantity, 'price_before_discount' => $priceBeforeDiscount, 'total_before_additions' => Currency::round($itemTotalWithDiscount), 'price' => $priceWithDiscount, 'order_id' => $order->getId(), - 'item_name' => $this->getOrderItemLabel($ticket, $ticketPriceDetails->price_id), + 'item_name' => $this->getOrderItemLabel($product, $productPriceDetails->price_id), 'total_tax' => $taxesAndFees->taxTotal, 'total_service_fee' => $taxesAndFees->feeTotal, 'total_gross' => Currency::round($itemTotalWithDiscount + $taxesAndFees->taxTotal + $taxesAndFees->feeTotal), @@ -108,14 +109,14 @@ private function calculateOrderItemData( ]; } - private function getOrderItemLabel(TicketDomainObject $ticket, int $priceId): string + private function getOrderItemLabel(ProductDomainObject $product, int $priceId): string { - if ($ticket->isTieredType()) { - return $ticket->getTitle() . ' - ' . $ticket->getTicketPrices() + if ($product->isTieredType()) { + return $product->getTitle() . ' - ' . $product->getProductPrices() ?->filter(fn($p) => $p->getId() === $priceId)->first() ?->getLabel(); } - return $ticket->getTitle(); + return $product->getTitle(); } } diff --git a/backend/app/Services/Domain/Payment/Stripe/EventHandlers/PaymentIntentSucceededHandler.php b/backend/app/Services/Domain/Payment/Stripe/EventHandlers/PaymentIntentSucceededHandler.php index 8f7f37b2..11bc97e9 100644 --- a/backend/app/Services/Domain/Payment/Stripe/EventHandlers/PaymentIntentSucceededHandler.php +++ b/backend/app/Services/Domain/Payment/Stripe/EventHandlers/PaymentIntentSucceededHandler.php @@ -19,7 +19,7 @@ use HiEvents\Repository\Eloquent\Value\Relationship; use HiEvents\Repository\Interfaces\OrderRepositoryInterface; use HiEvents\Services\Domain\Payment\Stripe\StripeRefundExpiredOrderService; -use HiEvents\Services\Domain\Ticket\TicketQuantityUpdateService; +use HiEvents\Services\Domain\Product\ProductQuantityUpdateService; use Illuminate\Database\DatabaseManager; use Stripe\Exception\ApiErrorException; use Stripe\PaymentIntent; @@ -30,7 +30,7 @@ public function __construct( private OrderRepositoryInterface $orderRepository, private StripePaymentsRepository $stripePaymentsRepository, - private TicketQuantityUpdateService $quantityUpdateService, + private ProductQuantityUpdateService $quantityUpdateService, private StripeRefundExpiredOrderService $refundExpiredOrderService, private DatabaseManager $databaseManager, ) @@ -93,8 +93,8 @@ private function updateStripePaymentInfo(PaymentIntent $paymentIntent, StripePay /** * If the order has expired (reserved_until is in the past), refund the payment and throw an exception. - * This does seem quite extreme, but it ensures we don't oversell tickets. As far as I can see - * this is how Ticketmaster and other ticketing systems work. + * This does seem quite extreme, but it ensures we don't oversell products. As far as I can see + * this is how Productmaster and other producting systems work. * * @throws ApiErrorException * @throws RoundingNecessaryException @@ -102,7 +102,7 @@ private function updateStripePaymentInfo(PaymentIntent $paymentIntent, StripePay * @throws MathException * @throws UnknownCurrencyException * @throws NumberFormatException - * @todo We could check to see if there are tickets available, and if so, complete the order. + * @todo We could check to see if there are products available, and if so, complete the order. * This would be a better user experience. * */ diff --git a/backend/app/Services/Domain/Payment/Stripe/StripePaymentIntentCreationService.php b/backend/app/Services/Domain/Payment/Stripe/StripePaymentIntentCreationService.php index be408ef9..343c8083 100644 --- a/backend/app/Services/Domain/Payment/Stripe/StripePaymentIntentCreationService.php +++ b/backend/app/Services/Domain/Payment/Stripe/StripePaymentIntentCreationService.php @@ -66,7 +66,6 @@ public function createPaymentIntent(CreatePaymentIntentRequestDTO $paymentIntent 'amount' => $paymentIntentDTO->amount, 'currency' => $paymentIntentDTO->currencyCode, 'customer' => $this->upsertStripeCustomer($paymentIntentDTO)->getStripeCustomerId(), - 'setup_future_usage' => 'on_session', 'metadata' => [ 'order_id' => $paymentIntentDTO->order->getId(), 'event_id' => $paymentIntentDTO->order->getEventId(), diff --git a/backend/app/Services/Domain/Product/AvailableProductQuantitiesFetchService.php b/backend/app/Services/Domain/Product/AvailableProductQuantitiesFetchService.php new file mode 100644 index 00000000..12e060e2 --- /dev/null +++ b/backend/app/Services/Domain/Product/AvailableProductQuantitiesFetchService.php @@ -0,0 +1,173 @@ +config->get('app.homepage_product_quantities_cache_ttl')) { + $cachedData = $this->getDataFromCache($eventId); + if ($cachedData) { + return $cachedData; + } + } + + $capacities = $this->capacityAssignmentRepository + ->loadRelation(ProductDomainObject::class) + ->findWhere([ + 'event_id' => $eventId, + 'applies_to' => CapacityAssignmentAppliesTo::PRODUCTS->name, + 'status' => CapacityAssignmentStatus::ACTIVE->name, + ]); + + $reservedProductQuantities = $this->fetchReservedProductQuantities($eventId); + $productCapacities = $this->calculateProductCapacities($capacities); + + $quantities = $reservedProductQuantities->map(function (AvailableProductQuantitiesDTO $dto) use ($productCapacities) { + $productId = $dto->product_id; + if (isset($productCapacities[$productId])) { + $dto->quantity_available = min(array_merge([$dto->quantity_available], $productCapacities[$productId]->map->getAvailableCapacity()->toArray())); + $dto->capacities = $productCapacities[$productId]; + } + + return $dto; + }); + + $finalData = new AvailableProductQuantitiesResponseDTO( + productQuantities: $quantities, + capacities: $capacities + ); + + if (!$ignoreCache && $this->config->get('app.homepage_product_quantities_cache_ttl')) { + $this->cache->put($this->getCacheKey($eventId), $finalData, $this->config->get('app.homepage_product_quantities_cache_ttl')); + } + + return $finalData; + } + + private function fetchReservedProductQuantities(int $eventId): Collection + { + $result = $this->db->select(<< NOW() + AND orders.deleted_at IS NULL + THEN order_items.quantity + ELSE 0 + END + ) AS quantity_reserved + FROM products + JOIN product_prices ON products.id = product_prices.product_id + LEFT JOIN order_items ON order_items.product_id = products.id + AND order_items.product_price_id = product_prices.id + LEFT JOIN orders ON orders.id = order_items.order_id + AND orders.event_id = products.event_id + AND orders.deleted_at IS NULL + WHERE + products.event_id = :eventId + AND products.deleted_at IS NULL + AND product_prices.deleted_at IS NULL + GROUP BY products.id, product_prices.id + ) + SELECT + products.id AS product_id, + product_prices.id AS product_price_id, + products.title AS product_title, + product_prices.label AS price_label, + product_prices.initial_quantity_available, + product_prices.quantity_sold, + COALESCE( + product_prices.initial_quantity_available + - product_prices.quantity_sold + - COALESCE(reserved_quantities.quantity_reserved, 0), + 0) AS quantity_available, + COALESCE(reserved_quantities.quantity_reserved, 0) AS quantity_reserved, + CASE WHEN product_prices.initial_quantity_available IS NULL + THEN TRUE + ELSE FALSE + END AS unlimited_quantity_available + FROM products + JOIN product_prices ON products.id = product_prices.product_id + LEFT JOIN reserved_quantities ON products.id = reserved_quantities.product_id + AND product_prices.id = reserved_quantities.product_price_id + WHERE + products.event_id = :eventId + AND products.deleted_at IS NULL + AND product_prices.deleted_at IS NULL + GROUP BY products.id, product_prices.id, reserved_quantities.quantity_reserved; + SQL, [ + 'eventId' => $eventId, + 'reserved' => OrderStatus::RESERVED->name + ]); + + return collect($result)->map(fn($row) => AvailableProductQuantitiesDTO::fromArray([ + 'product_id' => $row->product_id, + 'price_id' => $row->product_price_id, + 'product_title' => $row->product_title, + 'price_label' => $row->price_label, + 'quantity_available' => $row->unlimited_quantity_available ? Constants::INFINITE : $row->quantity_available, + 'initial_quantity_available' => $row->initial_quantity_available, + 'quantity_reserved' => $row->quantity_reserved, + 'capacities' => new Collection(), + ])); + } + + /** + * @param Collection $capacities + */ + private function calculateProductCapacities(Collection $capacities): array + { + $productCapacities = []; + foreach ($capacities as $capacity) { + foreach ($capacity->getProducts() as $product) { + $productId = $product->getId(); + if (!isset($productCapacities[$productId])) { + $productCapacities[$productId] = collect(); + } + + $productCapacities[$productId]->push($capacity); + } + } + + return $productCapacities; + } + + private function getDataFromCache(int $eventId): ?AvailableProductQuantitiesResponseDTO + { + return $this->cache->get($this->getCacheKey($eventId)); + } + + private function getCacheKey(int $eventId): string + { + return "event.$eventId.available_product_quantities"; + } +} diff --git a/backend/app/Services/Domain/Product/CreateProductService.php b/backend/app/Services/Domain/Product/CreateProductService.php new file mode 100644 index 00000000..22671398 --- /dev/null +++ b/backend/app/Services/Domain/Product/CreateProductService.php @@ -0,0 +1,124 @@ +databaseManager->transaction(function () use ($accountId, $taxAndFeeIds, $product) { + $persistedProduct = $this->persistProduct($product); + + if ($taxAndFeeIds) { + $this->associateTaxesAndFees($persistedProduct, $taxAndFeeIds, $accountId); + } + + return $this->createProductPrices($persistedProduct, $product); + }); + } + + private function persistProduct(ProductDomainObject $productsData): ProductDomainObject + { + $event = $this->eventRepository->findById($productsData->getEventId()); + + return $this->productRepository->create([ + 'title' => $productsData->getTitle(), + 'type' => $productsData->getType(), + 'product_type' => $productsData->getProductType(), + 'order' => $this->productOrderingService->getOrderForNewProduct( + eventId: $productsData->getEventId(), + productCategoryId: $productsData->getProductCategoryId(), + ), + 'sale_start_date' => $productsData->getSaleStartDate() + ? DateHelper::convertToUTC($productsData->getSaleStartDate(), $event->getTimezone()) + : null, + 'sale_end_date' => $productsData->getSaleEndDate() + ? DateHelper::convertToUTC($productsData->getSaleEndDate(), $event->getTimezone()) + : null, + 'max_per_order' => $productsData->getMaxPerOrder(), + 'description' => $this->purifier->purify($productsData->getDescription()), + 'start_collapsed' => $productsData->getStartCollapsed(), + 'min_per_order' => $productsData->getMinPerOrder(), + 'is_hidden' => $productsData->getIsHidden(), + 'hide_before_sale_start_date' => $productsData->getHideBeforeSaleStartDate(), + 'hide_after_sale_end_date' => $productsData->getHideAfterSaleEndDate(), + 'hide_when_sold_out' => $productsData->getHideWhenSoldOut(), + 'show_quantity_remaining' => $productsData->getShowQuantityRemaining(), + 'is_hidden_without_promo_code' => $productsData->getIsHiddenWithoutPromoCode(), + 'event_id' => $productsData->getEventId(), + 'product_category_id' => $productsData->getProductCategoryId(), + ]); + } + + /** + * @throws Exception + */ + private function createProductTaxesAndFees( + ProductDomainObject $product, + array $taxAndFeeIds, + int $accountId, + ): Collection + { + return $this->taxAndProductAssociationService->addTaxesToProduct( + new TaxAndProductAssociateParams( + productId: $product->getId(), + accountId: $accountId, + taxAndFeeIds: $taxAndFeeIds, + ), + ); + } + + /** + * @throws Exception + */ + private function associateTaxesAndFees(ProductDomainObject $persistedProduct, array $taxAndFeeIds, int $accountId): void + { + $persistedProduct->setTaxAndFees($this->createProductTaxesAndFees( + product: $persistedProduct, + taxAndFeeIds: $taxAndFeeIds, + accountId: $accountId, + )); + } + + private function createProductPrices(ProductDomainObject $persistedProduct, ProductDomainObject $product): ProductDomainObject + { + $prices = $this->priceCreateService->createPrices( + productId: $persistedProduct->getId(), + prices: $product->getProductPrices(), + event: $this->eventRepository->findById($product->getEventId()), + ); + + return $persistedProduct->setProductPrices($prices); + } +} diff --git a/backend/app/Services/Domain/Ticket/DTO/AvailableTicketQuantitiesDTO.php b/backend/app/Services/Domain/Product/DTO/AvailableProductQuantitiesDTO.php similarity index 74% rename from backend/app/Services/Domain/Ticket/DTO/AvailableTicketQuantitiesDTO.php rename to backend/app/Services/Domain/Product/DTO/AvailableProductQuantitiesDTO.php index 72c032b6..01c68aff 100644 --- a/backend/app/Services/Domain/Ticket/DTO/AvailableTicketQuantitiesDTO.php +++ b/backend/app/Services/Domain/Product/DTO/AvailableProductQuantitiesDTO.php @@ -1,17 +1,17 @@ */ - public Collection $ticketQuantities, + /** @var Collection */ + public Collection $productQuantities, /** @var Collection */ public ?Collection $capacities = null, ) diff --git a/backend/app/Services/Domain/Product/DTO/CreateProductDTO.php b/backend/app/Services/Domain/Product/DTO/CreateProductDTO.php new file mode 100644 index 00000000..aca5aab2 --- /dev/null +++ b/backend/app/Services/Domain/Product/DTO/CreateProductDTO.php @@ -0,0 +1,10 @@ +databaseManager->transaction(function () use ($productId, $eventId) { + if ($this->productRepository->hasAssociatedOrders($productId)) { + throw new CannotDeleteEntityException( + __('You cannot delete this product because it has orders associated with it. You can hide it instead.') + ); + } + + $this->productRepository->deleteWhere( + [ + ProductDomainObjectAbstract::EVENT_ID => $eventId, + ProductDomainObjectAbstract::ID => $productId, + ] + ); + + $this->productPriceRepository->deleteWhere( + [ + ProductPriceDomainObjectAbstract::PRODUCT_ID => $productId, + ] + ); + }); + + $this->logger->info( + sprintf('Product with id %d was deleted from event with id %d', $productId, $eventId), + [ + 'product_id' => $productId, + 'event_id' => $eventId, + ] + ); + } +} diff --git a/backend/app/Services/Domain/Product/EventProductValidationService.php b/backend/app/Services/Domain/Product/EventProductValidationService.php new file mode 100644 index 00000000..ae46396b --- /dev/null +++ b/backend/app/Services/Domain/Product/EventProductValidationService.php @@ -0,0 +1,35 @@ +productRepository->findWhere([ + 'event_id' => $eventId, + ])->map(fn(ProductDomainObject $product) => $product->getId()) + ->toArray(); + + $invalidProductIds = array_diff($productIds, $validProductIds); + + if (!empty($invalidProductIds)) { + throw new UnrecognizedProductIdException( + __('Invalid product ids: :ids', ['ids' => implode(', ', $invalidProductIds)]) + ); + } + } +} diff --git a/backend/app/Services/Domain/Product/Exception/UnrecognizedProductIdException.php b/backend/app/Services/Domain/Product/Exception/UnrecognizedProductIdException.php new file mode 100644 index 00000000..f788e3fe --- /dev/null +++ b/backend/app/Services/Domain/Product/Exception/UnrecognizedProductIdException.php @@ -0,0 +1,10 @@ + $productsCategories + * @param PromoCodeDomainObject|null $promoCode + * @param bool $hideSoldOutProducts + * @return Collection + */ + public function filter( + Collection $productsCategories, + ?PromoCodeDomainObject $promoCode = null, + bool $hideSoldOutProducts = true, + ): Collection + { + if ($productsCategories->isEmpty()) { + return $productsCategories; + } + + $products = $productsCategories + ->flatMap(fn(ProductCategoryDomainObject $category) => $category->getProducts()); + + if ($products->isEmpty()) { + return $productsCategories + ->reject(fn(ProductCategoryDomainObject $category) => $category->getIsHidden()); + } + + $productQuantities = $this + ->fetchAvailableProductQuantitiesService + ->getAvailableProductQuantities($products->first()->getEventId()); + + $filteredProducts = $products + ->map(fn(ProductDomainObject $product) => $this->processProduct($product, $productQuantities->productQuantities, $promoCode)) + ->reject(fn(ProductDomainObject $product) => $this->filterProduct($product, $promoCode, $hideSoldOutProducts)) + ->each(fn(ProductDomainObject $product) => $this->processProductPrices($product, $hideSoldOutProducts)); + + return $productsCategories + ->reject(fn(ProductCategoryDomainObject $category) => $category->getIsHidden()) + ->each(fn(ProductCategoryDomainObject $category) => $category->setProducts( + $filteredProducts->where( + static fn(ProductDomainObject $product) => $product->getProductCategoryId() === $category->getId() + ) + )); + } + + private function isHiddenByPromoCode(ProductDomainObject $product, ?PromoCodeDomainObject $promoCode): bool + { + return $product->getIsHiddenWithoutPromoCode() && !( + $promoCode + && $promoCode->appliesToProduct($product) + ); + } + + private function shouldProductBeDiscounted(?PromoCodeDomainObject $promoCode, ProductDomainObject $product): bool + { + if ($product->isDonationType() || $product->isFreeType()) { + return false; + } + + return $promoCode + && $promoCode->isDiscountCode() + && $promoCode->appliesToProduct($product); + } + + /** + * @param PromoCodeDomainObject|null $promoCode + * @param ProductDomainObject $product + * @param Collection $productQuantities + * @return ProductDomainObject + */ + private function processProduct( + ProductDomainObject $product, + Collection $productQuantities, + ?PromoCodeDomainObject $promoCode = null, + ): ProductDomainObject + { + if ($this->shouldProductBeDiscounted($promoCode, $product)) { + $product->getProductPrices()?->each(function (ProductPriceDomainObject $price) use ($product, $promoCode) { + $price->setPriceBeforeDiscount($price->getPrice()); + $price->setPrice($this->productPriceService->getIndividualPrice($product, $price, $promoCode)); + }); + } + + $product->getProductPrices()?->map(function (ProductPriceDomainObject $price) use ($productQuantities) { + $availableQuantity = $productQuantities->where('price_id', $price->getId())->first()?->quantity_available; + $availableQuantity = $availableQuantity === Constants::INFINITE ? null : $availableQuantity; + $price->setQuantityAvailable( + max($availableQuantity, 0) + ); + }); + + // If there is a capacity assigned to the product, we set the capacity to capacity available qty, or the sum of all + // product prices qty, whichever is lower + $productQuantities->each(function (AvailableProductQuantitiesDTO $quantity) use ($product) { + if ($quantity->capacities !== null && $quantity->capacities->isNotEmpty() && $quantity->product_id === $product->getId()) { + $product->setQuantityAvailable( + $quantity->capacities->min(fn(CapacityAssignmentDomainObject $capacity) => $capacity->getAvailableCapacity()) + ); + } + }); + + return $product; + } + + private function filterProduct( + ProductDomainObject $product, + ?PromoCodeDomainObject $promoCode = null, + bool $hideSoldOutProducts = true, + ): bool + { + $hidden = false; + + if ($this->isHiddenByPromoCode($product, $promoCode)) { + $product->setOffSaleReason(__('Product is hidden without promo code')); + $hidden = true; + } + + if ($product->isSoldOut() && $product->getHideWhenSoldOut()) { + $product->setOffSaleReason(__('Product is sold out')); + $hidden = true; + } + + if ($product->isBeforeSaleStartDate() && $product->getHideBeforeSaleStartDate()) { + $product->setOffSaleReason(__('Product is before sale start date')); + $hidden = true; + } + + if ($product->isAfterSaleEndDate() && $product->getHideAfterSaleEndDate()) { + $product->setOffSaleReason(__('Product is after sale end date')); + $hidden = true; + } + + if ($product->getIsHidden()) { + $product->setOffSaleReason(__('Product is hidden')); + $hidden = true; + } + + return $hidden && $hideSoldOutProducts; + } + + private function processProductPrice(ProductDomainObject $product, ProductPriceDomainObject $price): void + { + $taxAndFees = $this->taxCalculationService + ->calculateTaxAndFeesForProductPrice($product, $price); + + $price + ->setTaxTotal(Currency::round($taxAndFees->taxTotal)) + ->setFeeTotal(Currency::round($taxAndFees->feeTotal)); + + $price->setIsAvailable($this->getPriceAvailability($price, $product)); + } + + private function filterProductPrice( + ProductDomainObject $product, + ProductPriceDomainObject $price, + bool $hideSoldOutProducts = true + ): bool + { + $hidden = false; + + if (!$product->isTieredType()) { + return false; + } + + if ($price->isBeforeSaleStartDate() && $product->getHideBeforeSaleStartDate()) { + $price->setOffSaleReason(__('Price is before sale start date')); + $hidden = true; + } + + if ($price->isAfterSaleEndDate() && $product->getHideAfterSaleEndDate()) { + $price->setOffSaleReason(__('Price is after sale end date')); + $hidden = true; + } + + if ($price->isSoldOut() && $product->getHideWhenSoldOut()) { + $price->setOffSaleReason(__('Price is sold out')); + $hidden = true; + } + + if ($price->getIsHidden()) { + $price->setOffSaleReason(__('Price is hidden')); + $hidden = true; + } + + return $hidden && $hideSoldOutProducts; + } + + private function processProductPrices(ProductDomainObject $product, bool $hideSoldOutProducts = true): void + { + $product->setProductPrices( + $product->getProductPrices() + ?->each(fn(ProductPriceDomainObject $price) => $this->processProductPrice($product, $price)) + ->reject(fn(ProductPriceDomainObject $price) => $this->filterProductPrice($product, $price, $hideSoldOutProducts)) + ); + } + + /** + * For non-tiered products, we can inherit the availability of the product. + * + * @param ProductPriceDomainObject $price + * @param ProductDomainObject $product + * @return bool + */ + private function getPriceAvailability(ProductPriceDomainObject $price, ProductDomainObject $product): bool + { + if ($product->isTieredType()) { + return !$price->isSoldOut() + && !$price->isBeforeSaleStartDate() + && !$price->isAfterSaleEndDate() + && !$price->getIsHidden(); + } + + return !$product->isSoldOut() + && !$product->isBeforeSaleStartDate() + && !$product->isAfterSaleEndDate() + && !$product->getIsHidden(); + } +} diff --git a/backend/app/Services/Domain/Product/ProductOrderingService.php b/backend/app/Services/Domain/Product/ProductOrderingService.php new file mode 100644 index 00000000..11e9498a --- /dev/null +++ b/backend/app/Services/Domain/Product/ProductOrderingService.php @@ -0,0 +1,24 @@ +productRepository->findWhere([ + 'event_id' => $eventId, + 'product_category_id' => $productCategoryId, + ]) + ->max((static fn(ProductDomainObject $product) => $product->getOrder())) ?? 0) + 1; + } +} diff --git a/backend/app/Services/Domain/Ticket/TicketPriceCreateService.php b/backend/app/Services/Domain/Product/ProductPriceCreateService.php similarity index 66% rename from backend/app/Services/Domain/Ticket/TicketPriceCreateService.php rename to backend/app/Services/Domain/Product/ProductPriceCreateService.php index bff6de6d..57fae4a6 100644 --- a/backend/app/Services/Domain/Ticket/TicketPriceCreateService.php +++ b/backend/app/Services/Domain/Product/ProductPriceCreateService.php @@ -1,29 +1,29 @@ map(fn(TicketPriceDomainObject $price, int $index) => $this->ticketPriceRepository->create([ - 'ticket_id' => $ticketId, + return (new Collection($prices->map(fn(ProductPriceDomainObject $price, int $index) => $this->productPriceRepository->create([ + 'product_id' => $productId, 'price' => $price->getPrice(), 'label' => $price->getLabel(), 'sale_start_date' => $price->getSaleStartDate() diff --git a/backend/app/Services/Domain/Product/ProductPriceService.php b/backend/app/Services/Domain/Product/ProductPriceService.php new file mode 100644 index 00000000..aa29d655 --- /dev/null +++ b/backend/app/Services/Domain/Product/ProductPriceService.php @@ -0,0 +1,77 @@ +getPrice($product, new OrderProductPriceDTO( + quantity: 1, + price_id: $price->getId(), + ), $promoCode)->price; + } + + public function getPrice( + ProductDomainObject $product, + OrderProductPriceDTO $productOrderDetail, + ?PromoCodeDomainObject $promoCode + ): PriceDTO + { + $price = $this->determineProductPrice($product, $productOrderDetail); + + if ($product->getType() === ProductPriceType::FREE->name) { + return new PriceDTO(0.00); + } + + if ($product->getType() === ProductPriceType::DONATION->name) { + return new PriceDTO($price); + } + + if (!$promoCode || !$promoCode->appliesToProduct($product)) { + return new PriceDTO($price); + } + + if ($promoCode->getDiscountType() === PromoCodeDiscountTypeEnum::NONE->name) { + return new PriceDTO($price); + } + + if ($promoCode->isFixedDiscount()) { + $discountPrice = Currency::round($price - $promoCode->getDiscount()); + } elseif ($promoCode->isPercentageDiscount()) { + $discountPrice = Currency::round( + $price - ($price * ($promoCode->getDiscount() / 100)) + ); + } else { + $discountPrice = $price; + } + + return new PriceDTO( + price: max(0, $discountPrice), + price_before_discount: $price + ); + } + + private function determineProductPrice(ProductDomainObject $product, OrderProductPriceDTO $productOrderDetails): float + { + return match ($product->getType()) { + ProductPriceType::DONATION->name => max($product->getPrice(), $productOrderDetails->price), + ProductPriceType::PAID->name => $product->getPrice(), + ProductPriceType::FREE->name => 0.00, + ProductPriceType::TIERED->name => $product->getPriceById($productOrderDetails->price_id)?->getPrice() + }; + } +} diff --git a/backend/app/Services/Domain/Ticket/TicketPriceUpdateService.php b/backend/app/Services/Domain/Product/ProductPriceUpdateService.php similarity index 62% rename from backend/app/Services/Domain/Ticket/TicketPriceUpdateService.php rename to backend/app/Services/Domain/Product/ProductPriceUpdateService.php index f2bef504..92293a2a 100644 --- a/backend/app/Services/Domain/Ticket/TicketPriceUpdateService.php +++ b/backend/app/Services/Domain/Product/ProductPriceUpdateService.php @@ -1,22 +1,22 @@ $existingPrices */ - Collection $existingPrices, - EventDomainObject $event, + ProductDomainObject $product, + UpsertProductDTO $productsData, + /** @var Collection $existingPrices */ + Collection $existingPrices, + EventDomainObject $event, ): void { - if ($ticketsData->type !== TicketType::TIERED) { - $prices = new Collection([new TicketPriceDTO( - price: $ticketsData->type === TicketType::FREE ? 0.00 : $ticketsData->prices->first()->price, + if ($productsData->type !== ProductPriceType::TIERED) { + $prices = new Collection([new ProductPriceDTO( + price: $productsData->type === ProductPriceType::FREE ? 0.00 : $productsData->prices->first()->price, label: null, sale_start_date: null, sale_end_date: null, - initial_quantity_available: $ticketsData->prices->first()->initial_quantity_available, + initial_quantity_available: $productsData->prices->first()->initial_quantity_available, id: $existingPrices->first()->getId(), )]); } else { - $prices = $ticketsData->prices; + $prices = $productsData->prices; } $order = 1; foreach ($prices as $price) { if ($price->id === null) { - $this->ticketPriceRepository->create([ - 'ticket_id' => $ticket->getId(), + $this->productPriceRepository->create([ + 'product_id' => $product->getId(), 'price' => $price->price, 'label' => $price->label, 'sale_start_date' => $price->sale_start_date @@ -64,8 +64,8 @@ public function updatePrices( 'order' => $order++, ]); } else { - $this->ticketPriceRepository->updateWhere([ - 'ticket_id' => $ticket->getId(), + $this->productPriceRepository->updateWhere([ + 'product_id' => $product->getId(), 'price' => $price->price, 'label' => $price->label, 'sale_start_date' => $price->sale_start_date @@ -93,16 +93,16 @@ private function deletePrices(?Collection $prices, Collection $existingPrices): { $pricesIds = $prices?->map(fn($price) => $price->id)->toArray(); - $existingPrices->each(function (TicketPriceDomainObject $price) use ($pricesIds) { + $existingPrices->each(function (ProductPriceDomainObject $price) use ($pricesIds) { if (in_array($price->getId(), $pricesIds)) { return; } if ($price->getQuantitySold() > 0) { throw new CannotDeleteEntityException( - __('Cannot delete ticket price with id :id because it has sales', ['id' => $price->getId()]) + __('Cannot delete product price with id :id because it has sales', ['id' => $price->getId()]) ); } - $this->ticketPriceRepository->deleteById($price->getId()); + $this->productPriceRepository->deleteById($price->getId()); }); } } diff --git a/backend/app/Services/Domain/Ticket/TicketQuantityUpdateService.php b/backend/app/Services/Domain/Product/ProductQuantityUpdateService.php similarity index 80% rename from backend/app/Services/Domain/Ticket/TicketQuantityUpdateService.php rename to backend/app/Services/Domain/Product/ProductQuantityUpdateService.php index edfe2402..3dc6a09f 100644 --- a/backend/app/Services/Domain/Ticket/TicketQuantityUpdateService.php +++ b/backend/app/Services/Domain/Product/ProductQuantityUpdateService.php @@ -1,24 +1,24 @@ increaseCapacityAssignmentUsedCapacity($capacityAssignment->getId(), $adjustment); }); - $this->ticketPriceRepository->updateWhere([ + $this->productPriceRepository->updateWhere([ 'quantity_sold' => DB::raw('quantity_sold + ' . $adjustment), ], [ 'id' => $priceId, @@ -51,7 +51,7 @@ public function decreaseQuantitySold(int $priceId, int $adjustment = 1): void $this->decreaseCapacityAssignmentUsedCapacity($capacityAssignment->getId(), $adjustment); }); - $this->ticketPriceRepository->updateWhere([ + $this->productPriceRepository->updateWhere([ 'quantity_sold' => DB::raw('quantity_sold - ' . $adjustment), ], [ 'id' => $priceId, @@ -69,7 +69,7 @@ public function updateQuantitiesFromOrder(OrderDomainObject $order): void throw new InvalidArgumentException(__('Order has no order items')); } - $this->updateTicketQuantities($order); + $this->updateProductQuantities($order); }); } @@ -77,11 +77,11 @@ public function updateQuantitiesFromOrder(OrderDomainObject $order): void * @param OrderDomainObject $order * @return void */ - private function updateTicketQuantities(OrderDomainObject $order): void + private function updateProductQuantities(OrderDomainObject $order): void { /** @var OrderItemDomainObject $orderItem */ foreach ($order->getOrderItems() as $orderItem) { - $this->increaseQuantitySold($orderItem->getTicketPriceId(), $orderItem->getQuantity()); + $this->increaseQuantitySold($orderItem->getProductPriceId(), $orderItem->getQuantity()); } } @@ -109,10 +109,10 @@ private function decreaseCapacityAssignmentUsedCapacity(int $capacityAssignmentI */ private function getCapacityAssignments(int $priceId): Collection { - $price = $this->ticketPriceRepository->findFirstWhere([ + $price = $this->productPriceRepository->findFirstWhere([ 'id' => $priceId, ]); - return $this->ticketRepository->getCapacityAssignmentsByTicketId($price->getTicketId()); + return $this->productRepository->getCapacityAssignmentsByProductId($price->getProductId()); } } diff --git a/backend/app/Services/Domain/ProductCategory/CreateProductCategoryService.php b/backend/app/Services/Domain/ProductCategory/CreateProductCategoryService.php new file mode 100644 index 00000000..dcd4969d --- /dev/null +++ b/backend/app/Services/Domain/ProductCategory/CreateProductCategoryService.php @@ -0,0 +1,33 @@ +productCategoryRepository->create([ + 'name' => $name, + 'description' => $description, + 'is_hidden' => $isHidden, + 'event_id' => $eventId, + 'order' => $this->productCategoryRepository->getNextOrder($eventId), + 'no_products_message' => $noProductsMessage, + ]); + } +} diff --git a/backend/app/Services/Domain/ProductCategory/DeleteProductCategoryService.php b/backend/app/Services/Domain/ProductCategory/DeleteProductCategoryService.php new file mode 100644 index 00000000..f48765cf --- /dev/null +++ b/backend/app/Services/Domain/ProductCategory/DeleteProductCategoryService.php @@ -0,0 +1,117 @@ +databaseManager->transaction(function () use ($productCategoryId, $eventId) { + $this->handleDeletion($productCategoryId, $eventId); + }); + } + + /** + * @throws Throwable + * @throws CannotDeleteEntityException + */ + private function handleDeletion(int $productCategoryId, int $eventId): void + { + $this->validateCanDeleteProductCategory($eventId); + + $this->deleteCategoryProducts($productCategoryId, $eventId); + + $this->deleteCategory($productCategoryId, $eventId); + } + + /** + * @throws CannotDeleteEntityException + * @throws Throwable + */ + private function deleteCategoryProducts(int $productCategoryId, int $eventId): void + { + $productsToDelete = $this->productRepository->findWhere( + [ + ProductDomainObjectAbstract::PRODUCT_CATEGORY_ID => $productCategoryId, + ProductDomainObjectAbstract::EVENT_ID => $eventId, + ] + ); + + $productsWhichCanNotBeDeleted = new Collection(); + + foreach ($productsToDelete as $product) { + try { + $this->deleteProductService->deleteProduct($product->getId(), $eventId); + } catch (CannotDeleteEntityException) { + $productsWhichCanNotBeDeleted->push($product); + } + } + + if ($productsWhichCanNotBeDeleted->isNotEmpty()) { + throw new CannotDeleteEntityException( + __('You cannot delete this product category because it contains the following products: :products. These products are linked to existing orders. Please move the :product_name to another category before attempting to delete this one.', [ + 'products' => $productsWhichCanNotBeDeleted->map(fn($product) => $product->getTitle())->implode(', '), + 'product_name' => $productsWhichCanNotBeDeleted->count() > 1 ? __('products') : __('product'), + ]) + ); + } + } + + private function deleteCategory(int $productCategoryId, int $eventId): void + { + $this->productCategoryRepository->deleteWhere( + [ + ProductCategoryDomainObjectAbstract::ID => $productCategoryId, + ProductCategoryDomainObjectAbstract::EVENT_ID => $eventId, + ] + ); + + $this->logger->info(__('Product category :productCategoryId has been deleted.', [ + 'product_category_id' => $productCategoryId, + 'event_id' => $eventId, + ])); + } + + /** + * @throws CannotDeleteEntityException + */ + private function validateCanDeleteProductCategory(int $eventId): void + { + $existingRelatedCategories = $this->productCategoryRepository->findWhere( + [ + ProductCategoryDomainObjectAbstract::EVENT_ID => $eventId, + ] + ); + + if ($existingRelatedCategories->count() === 1) { + throw new CannotDeleteEntityException( + __('You cannot delete the last product category. Please create another category before deleting this one.') + ); + } + } +} diff --git a/backend/app/Services/Domain/ProductCategory/GetProductCategoryService.php b/backend/app/Services/Domain/ProductCategory/GetProductCategoryService.php new file mode 100644 index 00000000..ae03e4dd --- /dev/null +++ b/backend/app/Services/Domain/ProductCategory/GetProductCategoryService.php @@ -0,0 +1,47 @@ +productCategoryRepository + ->loadRelation(new Relationship( + domainObject: ProductDomainObject::class, + orderAndDirections: [ + new OrderAndDirection( + order: ProductCategoryDomainObjectAbstract::ORDER, + ), + ], + )) + ->findFirstWhere( + where: [ + 'id' => $categoryId, + 'event_id' => $eventId, + ] + ); + + if (!$category) { + throw new ResourceNotFoundException( + __('The product category with ID :id was not found.', ['id' => $categoryId]) + ); + } + + return $category; + } +} diff --git a/backend/app/Services/Domain/PromoCode/CreatePromoCodeService.php b/backend/app/Services/Domain/PromoCode/CreatePromoCodeService.php index 9e5609ce..d8cdaa5e 100644 --- a/backend/app/Services/Domain/PromoCode/CreatePromoCodeService.php +++ b/backend/app/Services/Domain/PromoCode/CreatePromoCodeService.php @@ -9,30 +9,30 @@ use HiEvents\Helper\DateHelper; use HiEvents\Repository\Interfaces\EventRepositoryInterface; use HiEvents\Repository\Interfaces\PromoCodeRepositoryInterface; -use HiEvents\Services\Domain\Ticket\EventTicketValidationService; -use HiEvents\Services\Domain\Ticket\Exception\UnrecognizedTicketIdException; +use HiEvents\Services\Domain\Product\EventProductValidationService; +use HiEvents\Services\Domain\Product\Exception\UnrecognizedProductIdException; class CreatePromoCodeService { public function __construct( - private readonly PromoCodeRepositoryInterface $promoCodeRepository, - private readonly EventTicketValidationService $eventTicketValidationService, - private readonly EventRepositoryInterface $eventRepository, + private readonly PromoCodeRepositoryInterface $promoCodeRepository, + private readonly EventProductValidationService $eventProductValidationService, + private readonly EventRepositoryInterface $eventRepository, ) { } /** * @throws ResourceConflictException - * @throws UnrecognizedTicketIdException + * @throws UnrecognizedProductIdException */ public function createPromoCode(PromoCodeDomainObject $promoCode): PromoCodeDomainObject { $this->checkForDuplicateCode($promoCode); - if (!empty($promoCode->getApplicableTicketIds())) { - $this->eventTicketValidationService->validateTicketIds( - ticketIds: $promoCode->getApplicableTicketIds(), + if (!empty($promoCode->getApplicableProductIds())) { + $this->eventProductValidationService->validateProductIds( + productIds: $promoCode->getApplicableProductIds(), eventId: $promoCode->getEventId() ); } @@ -50,7 +50,7 @@ public function createPromoCode(PromoCodeDomainObject $promoCode): PromoCodeDoma ? DateHelper::convertToUTC($promoCode->getExpiryDate(), $event->getTimezone()) : null, PromoCodeDomainObjectAbstract::MAX_ALLOWED_USAGES => $promoCode->getMaxAllowedUsages(), - PromoCodeDomainObjectAbstract::APPLICABLE_TICKET_IDS => $promoCode->getApplicableTicketIds(), + PromoCodeDomainObjectAbstract::APPLICABLE_PRODUCT_IDS => $promoCode->getApplicableProductIds(), ]); } diff --git a/backend/app/Services/Domain/Question/CreateQuestionService.php b/backend/app/Services/Domain/Question/CreateQuestionService.php index c50672de..42ef5ee6 100644 --- a/backend/app/Services/Domain/Question/CreateQuestionService.php +++ b/backend/app/Services/Domain/Question/CreateQuestionService.php @@ -24,7 +24,7 @@ public function __construct( */ public function createQuestion( QuestionDomainObject $question, - array $ticketIds, + array $productIds, ): QuestionDomainObject { return $this->databaseManager->transaction(fn() => $this->questionRepository->create([ @@ -36,6 +36,6 @@ public function createQuestion( QuestionDomainObjectAbstract::OPTIONS => $question->getOptions(), QuestionDomainObjectAbstract::IS_HIDDEN => $question->getIsHidden(), QuestionDomainObjectAbstract::DESCRIPTION => $this->purifier->purify($question->getDescription()), - ], $ticketIds)); + ], $productIds)); } } diff --git a/backend/app/Services/Domain/Question/EditQuestionService.php b/backend/app/Services/Domain/Question/EditQuestionService.php index 1c29a8d3..784fe931 100644 --- a/backend/app/Services/Domain/Question/EditQuestionService.php +++ b/backend/app/Services/Domain/Question/EditQuestionService.php @@ -4,7 +4,7 @@ use HiEvents\DomainObjects\Generated\QuestionDomainObjectAbstract; use HiEvents\DomainObjects\QuestionDomainObject; -use HiEvents\DomainObjects\TicketDomainObject; +use HiEvents\DomainObjects\ProductDomainObject; use HiEvents\Repository\Interfaces\QuestionRepositoryInterface; use HTMLPurifier; use Illuminate\Database\DatabaseManager; @@ -25,10 +25,10 @@ public function __construct( */ public function editQuestion( QuestionDomainObject $question, - array $ticketIds, + array $productIds, ): QuestionDomainObject { - return $this->databaseManager->transaction(function () use ($question, $ticketIds) { + return $this->databaseManager->transaction(function () use ($question, $productIds) { $this->questionRepository->updateQuestion( questionId: $question->getId(), eventId: $question->getEventId(), @@ -42,11 +42,11 @@ public function editQuestion( QuestionDomainObjectAbstract::IS_HIDDEN => $question->getIsHidden(), QuestionDomainObjectAbstract::DESCRIPTION => $this->purifier->purify($question->getDescription()), ], - ticketIds: $ticketIds + productIds: $productIds ); return $this->questionRepository - ->loadRelation(TicketDomainObject::class) + ->loadRelation(ProductDomainObject::class) ->findById($question->getId()); }); } diff --git a/backend/app/Services/Domain/Report/AbstractReportService.php b/backend/app/Services/Domain/Report/AbstractReportService.php new file mode 100644 index 00000000..24c3058a --- /dev/null +++ b/backend/app/Services/Domain/Report/AbstractReportService.php @@ -0,0 +1,49 @@ +eventRepository->findById($eventId); + $timezone = $event->getTimezone(); + + $endDate = Carbon::parse($endDate ?? now(), $timezone); + $startDate = Carbon::parse($startDate ?? $endDate->copy()->subDays(30), $timezone); + + $reportResults = $this->cache->remember( + key: $this->getCacheKey($eventId, $startDate, $endDate), + ttl: Carbon::now()->addSeconds(20), + callback: fn() => $this->queryBuilder->select( + $this->getSqlQuery($startDate, $endDate), + [ + 'event_id' => $eventId, + ] + ) + ); + + return collect($reportResults); + } + + abstract protected function getSqlQuery(Carbon $startDate, Carbon $endDate): string; + + protected function getCacheKey(int $eventId, ?Carbon $startDate, ?Carbon $endDate): string + { + return static::class . "$eventId.{$startDate?->toDateString()}.{$endDate?->toDateString()}"; + } +} diff --git a/backend/app/Services/Domain/Report/Exception/InvalidDateRange.php b/backend/app/Services/Domain/Report/Exception/InvalidDateRange.php new file mode 100644 index 00000000..56f5e6ba --- /dev/null +++ b/backend/app/Services/Domain/Report/Exception/InvalidDateRange.php @@ -0,0 +1,10 @@ + App::make(ProductSalesReport::class), + ReportTypes::DAILY_SALES_REPORT => App::make(DailySalesReport::class), + ReportTypes::PROMO_CODES_REPORT => App::make(PromoCodesReport::class), + }; + } +} diff --git a/backend/app/Services/Domain/Report/Reports/DailySalesReport.php b/backend/app/Services/Domain/Report/Reports/DailySalesReport.php new file mode 100644 index 00000000..ba396f37 --- /dev/null +++ b/backend/app/Services/Domain/Report/Reports/DailySalesReport.php @@ -0,0 +1,37 @@ +toDateString(); + $endDateStr = $endDate->toDateString(); + + return <<format('Y-m-d H:i:s'); + $endDateString = $endDate->format('Y-m-d H:i:s'); + $completedStatus = OrderStatus::COMPLETED->name; + + return <<format('Y-m-d H:i:s'); + $endDateString = $endDate->format('Y-m-d H:i:s'); + $reservedString = OrderStatus::RESERVED->name; + $translatedStringMap = [ + 'Expired' => __('Expired'), + 'Limit Reached' => __('Limit Reached'), + 'Deleted' => __('Deleted'), + 'Active' => __('Active'), + ]; + + return <<= '$startDateString' + AND o.created_at <= '$endDateString' + + GROUP BY + o.id, + o.promo_code_id, + o.promo_code, + o.total_gross, + o.email, + o.created_at + ), + promo_metrics AS ( + SELECT + COALESCE(pc.code, ot.promo_code) as promo_code, + COUNT(DISTINCT ot.order_id) as times_used, + COUNT(DISTINCT ot.email) as unique_customers, + COALESCE(SUM(ot.total_gross), 0) as total_gross_sales, + COALESCE(SUM(ot.original_total), 0) as total_before_discounts, + COALESCE(SUM(ot.original_total - ot.discounted_total), 0) as total_discount_amount, + MIN(ot.created_at AT TIME ZONE 'UTC') as first_used_at, + MAX(ot.created_at AT TIME ZONE 'UTC') as last_used_at, + pc.discount as configured_discount, + pc.discount_type, + pc.max_allowed_usages, + pc.expiry_date AT TIME ZONE 'UTC' as expiry_date, + CASE + WHEN pc.max_allowed_usages IS NOT NULL + THEN pc.max_allowed_usages - COUNT(ot.order_id)::integer + END as remaining_uses, + CASE + WHEN pc.expiry_date < CURRENT_TIMESTAMP THEN '{$translatedStringMap['Expired']}' + WHEN pc.max_allowed_usages IS NOT NULL AND COUNT(ot.order_id) >= pc.max_allowed_usages THEN '{$translatedStringMap['Limit Reached']}' + WHEN pc.deleted_at IS NOT NULL THEN '{$translatedStringMap['Deleted']}' + ELSE '{$translatedStringMap['Active']}' + END as status + FROM promo_codes pc + LEFT JOIN order_totals ot ON pc.id = ot.promo_code_id + WHERE + pc.deleted_at IS NULL + AND pc.event_id = :event_id + GROUP BY + pc.id, + COALESCE(pc.code, ot.promo_code), + pc.discount, + pc.discount_type, + pc.max_allowed_usages, + pc.expiry_date, + pc.deleted_at + ) + SELECT + promo_code, + times_used, + unique_customers, + configured_discount, + discount_type, + total_gross_sales, + total_before_discounts, + total_discount_amount, + first_used_at, + last_used_at, + max_allowed_usages, + remaining_uses, + expiry_date, + status + FROM promo_metrics + ORDER BY + total_gross_sales DESC, + promo_code; + SQL; + } +} diff --git a/backend/app/Services/Domain/Tax/DTO/TaxAndTicketAssociateParams.php b/backend/app/Services/Domain/Tax/DTO/TaxAndProductAssociateParams.php similarity index 71% rename from backend/app/Services/Domain/Tax/DTO/TaxAndTicketAssociateParams.php rename to backend/app/Services/Domain/Tax/DTO/TaxAndProductAssociateParams.php index cd645e87..f3b2b5fa 100644 --- a/backend/app/Services/Domain/Tax/DTO/TaxAndTicketAssociateParams.php +++ b/backend/app/Services/Domain/Tax/DTO/TaxAndProductAssociateParams.php @@ -2,10 +2,10 @@ namespace HiEvents\Services\Domain\Tax\DTO; -class TaxAndTicketAssociateParams +class TaxAndProductAssociateParams { public function __construct( - public readonly int $ticketId, + public readonly int $productId, public readonly int $accountId, public readonly array $taxAndFeeIds, ) diff --git a/backend/app/Services/Domain/Tax/TaxAndFeeCalculationService.php b/backend/app/Services/Domain/Tax/TaxAndFeeCalculationService.php index 641ad7d9..f3343269 100644 --- a/backend/app/Services/Domain/Tax/TaxAndFeeCalculationService.php +++ b/backend/app/Services/Domain/Tax/TaxAndFeeCalculationService.php @@ -4,8 +4,8 @@ use HiEvents\DomainObjects\Enums\TaxCalculationType; use HiEvents\DomainObjects\TaxAndFeesDomainObject; -use HiEvents\DomainObjects\TicketDomainObject; -use HiEvents\DomainObjects\TicketPriceDomainObject; +use HiEvents\DomainObjects\ProductDomainObject; +use HiEvents\DomainObjects\ProductPriceDomainObject; use HiEvents\Services\Domain\Tax\DTO\TaxCalculationResponse; use InvalidArgumentException; @@ -18,26 +18,26 @@ public function __construct(TaxAndFeeRollupService $taxRollupService) $this->taxRollupService = $taxRollupService; } - public function calculateTaxAndFeesForTicketPrice( - TicketDomainObject $ticket, - TicketPriceDomainObject $price, + public function calculateTaxAndFeesForProductPrice( + ProductDomainObject $product, + ProductPriceDomainObject $price, ): TaxCalculationResponse { - return $this->calculateTaxAndFeesForTicket($ticket, $price->getPrice()); + return $this->calculateTaxAndFeesForProduct($product, $price->getPrice()); } - public function calculateTaxAndFeesForTicket( - TicketDomainObject $ticket, - float $price, - int $quantity = 1 + public function calculateTaxAndFeesForProduct( + ProductDomainObject $product, + float $price, + int $quantity = 1 ): TaxCalculationResponse { $this->taxRollupService->resetRollUp(); - $fees = $ticket->getFees() + $fees = $product->getFees() ?->sum(fn($taxOrFee) => $this->calculateFee($taxOrFee, $price, $quantity)) ?: 0.00; - $taxFees = $ticket->getTaxRates() + $taxFees = $product->getTaxRates() ?->sum(fn($taxOrFee) => $this->calculateFee($taxOrFee, $price + $fees, $quantity)); return new TaxCalculationResponse( diff --git a/backend/app/Services/Domain/Tax/TaxAndTicketAssociationService.php b/backend/app/Services/Domain/Tax/TaxAndProductAssociationService.php similarity index 66% rename from backend/app/Services/Domain/Tax/TaxAndTicketAssociationService.php rename to backend/app/Services/Domain/Tax/TaxAndProductAssociationService.php index f59431fa..05527137 100644 --- a/backend/app/Services/Domain/Tax/TaxAndTicketAssociationService.php +++ b/backend/app/Services/Domain/Tax/TaxAndProductAssociationService.php @@ -4,16 +4,16 @@ use Exception; use HiEvents\Exceptions\InvalidTaxOrFeeIdException; +use HiEvents\Repository\Interfaces\ProductRepositoryInterface; use HiEvents\Repository\Interfaces\TaxAndFeeRepositoryInterface; -use HiEvents\Repository\Interfaces\TicketRepositoryInterface; -use HiEvents\Services\Domain\Tax\DTO\TaxAndTicketAssociateParams; +use HiEvents\Services\Domain\Tax\DTO\TaxAndProductAssociateParams; use Illuminate\Support\Collection; -readonly class TaxAndTicketAssociationService +readonly class TaxAndProductAssociationService { public function __construct( private TaxAndFeeRepositoryInterface $taxAndFeeRepository, - private TicketRepositoryInterface $ticketRepository, + private ProductRepositoryInterface $ticketRepository, ) { } @@ -21,7 +21,7 @@ public function __construct( /** * @throws Exception */ - public function addTaxesToTicket(TaxAndTicketAssociateParams $params): Collection + public function addTaxesToProduct(TaxAndProductAssociateParams $params): Collection { $taxesAndFees = $this->taxAndFeeRepository->findWhereIn( field: 'id', @@ -36,7 +36,7 @@ public function addTaxesToTicket(TaxAndTicketAssociateParams $params): Collectio throw new InvalidTaxOrFeeIdException(__('One or more tax IDs are invalid')); } - $this->ticketRepository->addTaxesAndFeesToTicket($params->ticketId, $params->taxAndFeeIds); + $this->ticketRepository->addTaxesAndFeesToProduct($params->productId, $params->taxAndFeeIds); return $taxesAndFees; } diff --git a/backend/app/Services/Domain/Ticket/AvailableTicketQuantitiesFetchService.php b/backend/app/Services/Domain/Ticket/AvailableTicketQuantitiesFetchService.php deleted file mode 100644 index e3cca4ac..00000000 --- a/backend/app/Services/Domain/Ticket/AvailableTicketQuantitiesFetchService.php +++ /dev/null @@ -1,173 +0,0 @@ -config->get('app.homepage_ticket_quantities_cache_ttl')) { - $cachedData = $this->getDataFromCache($eventId); - if ($cachedData) { - return $cachedData; - } - } - - $capacities = $this->capacityAssignmentRepository - ->loadRelation(TicketDomainObject::class) - ->findWhere([ - 'event_id' => $eventId, - 'applies_to' => CapacityAssignmentAppliesTo::TICKETS->name, - 'status' => CapacityAssignmentStatus::ACTIVE->name, - ]); - - $reservedTicketQuantities = $this->fetchReservedTicketQuantities($eventId); - $ticketCapacities = $this->calculateTicketCapacities($capacities); - - $quantities = $reservedTicketQuantities->map(function (AvailableTicketQuantitiesDTO $dto) use ($ticketCapacities) { - $ticketId = $dto->ticket_id; - if (isset($ticketCapacities[$ticketId])) { - $dto->quantity_available = min(array_merge([$dto->quantity_available], $ticketCapacities[$ticketId]->map->getAvailableCapacity()->toArray())); - $dto->capacities = $ticketCapacities[$ticketId]; - } - - return $dto; - }); - - $finalData = new AvailableTicketQuantitiesResponseDTO( - ticketQuantities: $quantities, - capacities: $capacities - ); - - if (!$ignoreCache && $this->config->get('app.homepage_ticket_quantities_cache_ttl')) { - $this->cache->put($this->getCacheKey($eventId), $finalData, $this->config->get('app.homepage_ticket_quantities_cache_ttl')); - } - - return $finalData; - } - - private function fetchReservedTicketQuantities(int $eventId): Collection - { - $result = $this->db->select(<< NOW() - AND orders.deleted_at IS NULL - THEN order_items.quantity - ELSE 0 - END - ) AS quantity_reserved - FROM tickets - JOIN ticket_prices ON tickets.id = ticket_prices.ticket_id - LEFT JOIN order_items ON order_items.ticket_id = tickets.id - AND order_items.ticket_price_id = ticket_prices.id - LEFT JOIN orders ON orders.id = order_items.order_id - AND orders.event_id = tickets.event_id - AND orders.deleted_at IS NULL - WHERE - tickets.event_id = :eventId - AND tickets.deleted_at IS NULL - AND ticket_prices.deleted_at IS NULL - GROUP BY tickets.id, ticket_prices.id - ) - SELECT - tickets.id AS ticket_id, - ticket_prices.id AS ticket_price_id, - tickets.title AS ticket_title, - ticket_prices.label AS price_label, - ticket_prices.initial_quantity_available, - ticket_prices.quantity_sold, - COALESCE( - ticket_prices.initial_quantity_available - - ticket_prices.quantity_sold - - COALESCE(reserved_quantities.quantity_reserved, 0), - 0) AS quantity_available, - COALESCE(reserved_quantities.quantity_reserved, 0) AS quantity_reserved, - CASE WHEN ticket_prices.initial_quantity_available IS NULL - THEN TRUE - ELSE FALSE - END AS unlimited_quantity_available - FROM tickets - JOIN ticket_prices ON tickets.id = ticket_prices.ticket_id - LEFT JOIN reserved_quantities ON tickets.id = reserved_quantities.ticket_id - AND ticket_prices.id = reserved_quantities.ticket_price_id - WHERE - tickets.event_id = :eventId - AND tickets.deleted_at IS NULL - AND ticket_prices.deleted_at IS NULL - GROUP BY tickets.id, ticket_prices.id, reserved_quantities.quantity_reserved; - SQL, [ - 'eventId' => $eventId, - 'reserved' => OrderStatus::RESERVED->name - ]); - - return collect($result)->map(fn($row) => AvailableTicketQuantitiesDTO::fromArray([ - 'ticket_id' => $row->ticket_id, - 'price_id' => $row->ticket_price_id, - 'ticket_title' => $row->ticket_title, - 'price_label' => $row->price_label, - 'quantity_available' => $row->unlimited_quantity_available ? Constants::INFINITE : $row->quantity_available, - 'initial_quantity_available' => $row->initial_quantity_available, - 'quantity_reserved' => $row->quantity_reserved, - 'capacities' => new Collection(), - ])); - } - - /** - * @param Collection $capacities - */ - private function calculateTicketCapacities(Collection $capacities): array - { - $ticketCapacities = []; - foreach ($capacities as $capacity) { - foreach ($capacity->getTickets() as $ticket) { - $ticketId = $ticket->getId(); - if (!isset($ticketCapacities[$ticketId])) { - $ticketCapacities[$ticketId] = collect(); - } - - $ticketCapacities[$ticketId]->push($capacity); - } - } - - return $ticketCapacities; - } - - private function getDataFromCache(int $eventId): ?AvailableTicketQuantitiesResponseDTO - { - return $this->cache->get($this->getCacheKey($eventId)); - } - - private function getCacheKey(int $eventId): string - { - return "event.$eventId.available_ticket_quantities"; - } -} diff --git a/backend/app/Services/Domain/Ticket/CreateTicketService.php b/backend/app/Services/Domain/Ticket/CreateTicketService.php deleted file mode 100644 index dc676480..00000000 --- a/backend/app/Services/Domain/Ticket/CreateTicketService.php +++ /dev/null @@ -1,118 +0,0 @@ -databaseManager->transaction(function () use ($accountId, $taxAndFeeIds, $ticket) { - $persistedTicket = $this->persistTicket($ticket); - - if ($taxAndFeeIds) { - $this->associateTaxesAndFees($persistedTicket, $taxAndFeeIds, $accountId); - } - - return $this->createTicketPrices($persistedTicket, $ticket); - }); - } - - private function persistTicket(TicketDomainObject $ticketsData): TicketDomainObject - { - $event = $this->eventRepository->findById($ticketsData->getEventId()); - - return $this->ticketRepository->create([ - 'title' => $ticketsData->getTitle(), - 'type' => $ticketsData->getType(), - 'order' => $ticketsData->getOrder(), - 'sale_start_date' => $ticketsData->getSaleStartDate() - ? DateHelper::convertToUTC($ticketsData->getSaleStartDate(), $event->getTimezone()) - : null, - 'sale_end_date' => $ticketsData->getSaleEndDate() - ? DateHelper::convertToUTC($ticketsData->getSaleEndDate(), $event->getTimezone()) - : null, - 'max_per_order' => $ticketsData->getMaxPerOrder(), - 'description' => $this->purifier->purify($ticketsData->getDescription()), - 'min_per_order' => $ticketsData->getMinPerOrder(), - 'is_hidden' => $ticketsData->getIsHidden(), - 'hide_before_sale_start_date' => $ticketsData->getHideBeforeSaleStartDate(), - 'hide_after_sale_end_date' => $ticketsData->getHideAfterSaleEndDate(), - 'hide_when_sold_out' => $ticketsData->getHideWhenSoldOut(), - 'start_collapsed' => $ticketsData->getStartCollapsed(), - 'show_quantity_remaining' => $ticketsData->getShowQuantityRemaining(), - 'is_hidden_without_promo_code' => $ticketsData->getIsHiddenWithoutPromoCode(), - 'event_id' => $ticketsData->getEventId(), - ]); - } - - /** - * @throws Exception - */ - private function createTicketTaxesAndFees( - TicketDomainObject $ticket, - array $taxAndFeeIds, - int $accountId, - ): Collection - { - return $this->taxAndTicketAssociationService->addTaxesToTicket( - new TaxAndTicketAssociateParams( - ticketId: $ticket->getId(), - accountId: $accountId, - taxAndFeeIds: $taxAndFeeIds, - ), - ); - } - - /** - * @throws Exception - */ - private function associateTaxesAndFees(TicketDomainObject $persistedTicket, array $taxAndFeeIds, int $accountId): void - { - $persistedTicket->setTaxAndFees($this->createTicketTaxesAndFees( - ticket: $persistedTicket, - taxAndFeeIds: $taxAndFeeIds, - accountId: $accountId, - )); - } - - private function createTicketPrices(TicketDomainObject $persistedTicket, TicketDomainObject $ticket): TicketDomainObject - { - $prices = $this->priceCreateService->createPrices( - ticketId: $persistedTicket->getId(), - prices: $ticket->getTicketPrices(), - event: $this->eventRepository->findById($ticket->getEventId()), - ); - - return $persistedTicket->setTicketPrices($prices); - } -} diff --git a/backend/app/Services/Domain/Ticket/DTO/CreateTicketDTO.php b/backend/app/Services/Domain/Ticket/DTO/CreateTicketDTO.php deleted file mode 100644 index d2008ba4..00000000 --- a/backend/app/Services/Domain/Ticket/DTO/CreateTicketDTO.php +++ /dev/null @@ -1,10 +0,0 @@ -ticketRepository->findWhere([ - 'event_id' => $eventId, - ])->map(fn(TicketDomainObject $ticket) => $ticket->getId()) - ->toArray(); - - $invalidTicketIds = array_diff($ticketIds, $validTicketIds); - - if (!empty($invalidTicketIds)) { - throw new UnrecognizedTicketIdException( - __('Invalid ticket ids: :ids', ['ids' => implode(', ', $invalidTicketIds)]) - ); - } - } -} diff --git a/backend/app/Services/Domain/Ticket/Exception/UnrecognizedTicketIdException.php b/backend/app/Services/Domain/Ticket/Exception/UnrecognizedTicketIdException.php deleted file mode 100644 index ed1ae38f..00000000 --- a/backend/app/Services/Domain/Ticket/Exception/UnrecognizedTicketIdException.php +++ /dev/null @@ -1,10 +0,0 @@ - $tickets - * @param PromoCodeDomainObject|null $promoCode - * @param bool $hideSoldOutTickets - * @return Collection - */ - public function filter( - Collection $tickets, - ?PromoCodeDomainObject $promoCode = null, - bool $hideSoldOutTickets = true, - ): Collection - { - if ($tickets->isEmpty()) { - return $tickets; - } - - $ticketQuantities = $this - ->fetchAvailableTicketQuantitiesService - ->getAvailableTicketQuantities($tickets->first()->getEventId()); - - return $tickets - ->map(fn(TicketDomainObject $ticket) => $this->processTicket($ticket, $ticketQuantities->ticketQuantities, $promoCode)) - ->reject(fn(TicketDomainObject $ticket) => $this->filterTicket($ticket, $promoCode, $hideSoldOutTickets)) - ->each(fn(TicketDomainObject $ticket) => $this->processTicketPrices($ticket, $hideSoldOutTickets)); - } - - private function isHiddenByPromoCode(TicketDomainObject $ticket, ?PromoCodeDomainObject $promoCode): bool - { - return $ticket->getIsHiddenWithoutPromoCode() && !( - $promoCode - && $promoCode->appliesToTicket($ticket) - ); - } - - private function shouldTicketBeDiscounted(?PromoCodeDomainObject $promoCode, TicketDomainObject $ticket): bool - { - if ($ticket->isDonationType() || $ticket->isFreeType()) { - return false; - } - - return $promoCode - && $promoCode->isDiscountCode() - && $promoCode->appliesToTicket($ticket); - } - - /** - * @param PromoCodeDomainObject|null $promoCode - * @param TicketDomainObject $ticket - * @param Collection $ticketQuantities - * @return TicketDomainObject - */ - private function processTicket( - TicketDomainObject $ticket, - Collection $ticketQuantities, - ?PromoCodeDomainObject $promoCode = null, - ): TicketDomainObject - { - if ($this->shouldTicketBeDiscounted($promoCode, $ticket)) { - $ticket->getTicketPrices()?->each(function (TicketPriceDomainObject $price) use ($ticket, $promoCode) { - $price->setPriceBeforeDiscount($price->getPrice()); - $price->setPrice($this->ticketPriceService->getIndividualPrice($ticket, $price, $promoCode)); - }); - } - - $ticket->getTicketPrices()?->map(function (TicketPriceDomainObject $price) use ($ticketQuantities) { - $availableQuantity = $ticketQuantities->where('price_id', $price->getId())->first()?->quantity_available; - $availableQuantity = $availableQuantity === Constants::INFINITE ? null : $availableQuantity; - $price->setQuantityAvailable( - max($availableQuantity, 0) - ); - }); - - // If there is a capacity assigned to the ticket, we set the capacity to capacity available qty, or the sum of all - // ticket prices qty, whichever is lower - $ticketQuantities->each(function (AvailableTicketQuantitiesDTO $quantity) use ($ticket) { - if ($quantity->capacities !== null && $quantity->capacities->isNotEmpty() && $quantity->ticket_id === $ticket->getId()) { - $ticket->setQuantityAvailable( - $quantity->capacities->min(fn(CapacityAssignmentDomainObject $capacity) => $capacity->getAvailableCapacity()) - ); - } - }); - - return $ticket; - } - - private function filterTicket( - TicketDomainObject $ticket, - ?PromoCodeDomainObject $promoCode = null, - bool $hideSoldOutTickets = true, - ): bool - { - $hidden = false; - - if ($this->isHiddenByPromoCode($ticket, $promoCode)) { - $ticket->setOffSaleReason(__('Ticket is hidden without promo code')); - $hidden = true; - } - - if ($ticket->isSoldOut() && $ticket->getHideWhenSoldOut()) { - $ticket->setOffSaleReason(__('Ticket is sold out')); - $hidden = true; - } - - if ($ticket->isBeforeSaleStartDate() && $ticket->getHideBeforeSaleStartDate()) { - $ticket->setOffSaleReason(__('Ticket is before sale start date')); - $hidden = true; - } - - if ($ticket->isAfterSaleEndDate() && $ticket->getHideAfterSaleEndDate()) { - $ticket->setOffSaleReason(__('Ticket is after sale end date')); - $hidden = true; - } - - if ($ticket->getIsHidden()) { - $ticket->setOffSaleReason(__('Ticket is hidden')); - $hidden = true; - } - - return $hidden && $hideSoldOutTickets; - } - - private function processTicketPrice(TicketDomainObject $ticket, TicketPriceDomainObject $price): void - { - $taxAndFees = $this->taxCalculationService - ->calculateTaxAndFeesForTicketPrice($ticket, $price); - - $price - ->setTaxTotal(Currency::round($taxAndFees->taxTotal)) - ->setFeeTotal(Currency::round($taxAndFees->feeTotal)); - - $price->setIsAvailable($this->getPriceAvailability($price, $ticket)); - } - - private function filterTicketPrice( - TicketDomainObject $ticket, - TicketPriceDomainObject $price, - bool $hideSoldOutTickets = true - ): bool - { - $hidden = false; - - if (!$ticket->isTieredType()) { - return false; - } - - if ($price->isBeforeSaleStartDate() && $ticket->getHideBeforeSaleStartDate()) { - $price->setOffSaleReason(__('Price is before sale start date')); - $hidden = true; - } - - if ($price->isAfterSaleEndDate() && $ticket->getHideAfterSaleEndDate()) { - $price->setOffSaleReason(__('Price is after sale end date')); - $hidden = true; - } - - if ($price->isSoldOut() && $ticket->getHideWhenSoldOut()) { - $price->setOffSaleReason(__('Price is sold out')); - $hidden = true; - } - - if ($price->getIsHidden()) { - $price->setOffSaleReason(__('Price is hidden')); - $hidden = true; - } - - return $hidden && $hideSoldOutTickets; - } - - private function processTicketPrices(TicketDomainObject $ticket, bool $hideSoldOutTickets = true): void - { - $ticket->setTicketPrices( - $ticket->getTicketPrices() - ?->each(fn(TicketPriceDomainObject $price) => $this->processTicketPrice($ticket, $price)) - ->reject(fn(TicketPriceDomainObject $price) => $this->filterTicketPrice($ticket, $price, $hideSoldOutTickets)) - ); - } - - /** - * For non-tiered tickets, we can inherit the availability of the ticket. - * - * @param TicketPriceDomainObject $price - * @param TicketDomainObject $ticket - * @return bool - */ - private function getPriceAvailability(TicketPriceDomainObject $price, TicketDomainObject $ticket): bool - { - if ($ticket->isTieredType()) { - return !$price->isSoldOut() - && !$price->isBeforeSaleStartDate() - && !$price->isAfterSaleEndDate() - && !$price->getIsHidden(); - } - - return !$ticket->isSoldOut() - && !$ticket->isBeforeSaleStartDate() - && !$ticket->isAfterSaleEndDate() - && !$ticket->getIsHidden(); - } -} diff --git a/backend/app/Services/Domain/Ticket/TicketPriceService.php b/backend/app/Services/Domain/Ticket/TicketPriceService.php deleted file mode 100644 index b2d652f1..00000000 --- a/backend/app/Services/Domain/Ticket/TicketPriceService.php +++ /dev/null @@ -1,77 +0,0 @@ -getPrice($ticket, new OrderTicketPriceDTO( - quantity: 1, - price_id: $price->getId(), - ), $promoCode)->price; - } - - public function getPrice( - TicketDomainObject $ticket, - OrderTicketPriceDTO $ticketOrderDetail, - ?PromoCodeDomainObject $promoCode - ): PriceDTO - { - $price = $this->determineTicketPrice($ticket, $ticketOrderDetail); - - if ($ticket->getType() === TicketType::FREE->name) { - return new PriceDTO(0.00); - } - - if ($ticket->getType() === TicketType::DONATION->name) { - return new PriceDTO($price); - } - - if (!$promoCode || !$promoCode->appliesToTicket($ticket)) { - return new PriceDTO($price); - } - - if ($promoCode->getDiscountType() === PromoCodeDiscountTypeEnum::NONE->name) { - return new PriceDTO($price); - } - - if ($promoCode->isFixedDiscount()) { - $discountPrice = Currency::round($price - $promoCode->getDiscount()); - } elseif ($promoCode->isPercentageDiscount()) { - $discountPrice = Currency::round( - $price - ($price * ($promoCode->getDiscount() / 100)) - ); - } else { - $discountPrice = $price; - } - - return new PriceDTO( - price: max(0, $discountPrice), - price_before_discount: $price - ); - } - - private function determineTicketPrice(TicketDomainObject $ticket, OrderTicketPriceDTO $ticketOrderDetails): float - { - return match ($ticket->getType()) { - TicketType::DONATION->name => max($ticket->getPrice(), $ticketOrderDetails->price), - TicketType::PAID->name => $ticket->getPrice(), - TicketType::FREE->name => 0.00, - TicketType::TIERED->name => $ticket->getPriceById($ticketOrderDetails->price_id)?->getPrice() - }; - } -} diff --git a/backend/app/Services/Handlers/Attendee/DTO/EditAttendeeDTO.php b/backend/app/Services/Handlers/Attendee/DTO/EditAttendeeDTO.php deleted file mode 100644 index 79b48659..00000000 --- a/backend/app/Services/Handlers/Attendee/DTO/EditAttendeeDTO.php +++ /dev/null @@ -1,20 +0,0 @@ -databaseManager->transaction(function () use ($editAttendeeDTO) { - $this->validateTicketId($editAttendeeDTO); - - $attendee = $this->getAttendee($editAttendeeDTO); - - $this->adjustTicketQuantities($attendee, $editAttendeeDTO); - - return $this->updateAttendee($editAttendeeDTO); - }); - } - - private function adjustTicketQuantities(AttendeeDomainObject $attendee, EditAttendeeDTO $editAttendeeDTO): void - { - if ($attendee->getTicketPriceId() !== $editAttendeeDTO->ticket_price_id) { - $this->ticketQuantityService->decreaseQuantitySold($attendee->getTicketPriceId()); - $this->ticketQuantityService->increaseQuantitySold($editAttendeeDTO->ticket_price_id); - } - } - - private function updateAttendee(EditAttendeeDTO $editAttendeeDTO): AttendeeDomainObject - { - return $this->attendeeRepository->updateByIdWhere($editAttendeeDTO->attendee_id, [ - 'first_name' => $editAttendeeDTO->first_name, - 'last_name' => $editAttendeeDTO->last_name, - 'email' => $editAttendeeDTO->email, - 'ticket_id' => $editAttendeeDTO->ticket_id, - 'ticket_price_id' => $editAttendeeDTO->ticket_price_id, - ], [ - 'event_id' => $editAttendeeDTO->event_id, - ]); - } - - /** - * @throws ValidationException - * @throws NoTicketsAvailableException - */ - private function validateTicketId(EditAttendeeDTO $editAttendeeDTO): void - { - $ticket = $this->ticketRepository - ->loadRelation(TicketPriceDomainObject::class) - ->findFirstWhere([ - TicketDomainObjectAbstract::ID => $editAttendeeDTO->ticket_id, - ]); - - if ($ticket->getEventId() !== $editAttendeeDTO->event_id) { - throw ValidationException::withMessages([ - 'ticket_id' => __('Ticket ID is not valid'), - ]); - } - - $ticketPriceIds = $ticket->getTicketPrices()->map(fn($ticketPrice) => $ticketPrice->getId())->toArray(); - if (!in_array($editAttendeeDTO->ticket_price_id, $ticketPriceIds, true)) { - throw ValidationException::withMessages([ - 'ticket_price_id' => __('Ticket price ID is not valid'), - ]); - } - - $availableQuantity = $this->ticketRepository->getQuantityRemainingForTicketPrice( - ticketId: $editAttendeeDTO->ticket_id, - ticketPriceId: $ticket->getType() === TicketType::TIERED->name - ? $editAttendeeDTO->ticket_price_id - : $ticket->getTicketPrices()->first()->getId(), - ); - - if ($availableQuantity <= 0) { - throw new NoTicketsAvailableException( - __('There are no tickets available. If you would like to assign this ticket to this attendee, please adjust the ticket\'s available quantity.') - ); - } - } - - /** - * @throws ValidationException - */ - private function getAttendee(EditAttendeeDTO $editAttendeeDTO): AttendeeDomainObject - { - $attendee = $this->attendeeRepository->findFirstWhere([ - AttendeeDomainObjectAbstract::EVENT_ID => $editAttendeeDTO->event_id, - AttendeeDomainObjectAbstract::ID => $editAttendeeDTO->attendee_id, - ]); - - if ($attendee === null) { - throw ValidationException::withMessages([ - 'attendee_id' => __('Attendee ID is not valid'), - ]); - } - - return $attendee; - } -} diff --git a/backend/app/Services/Handlers/Order/DTO/CompleteOrderAttendeeDTO.php b/backend/app/Services/Handlers/Order/DTO/CompleteOrderAttendeeDTO.php deleted file mode 100644 index 9d7bb32b..00000000 --- a/backend/app/Services/Handlers/Order/DTO/CompleteOrderAttendeeDTO.php +++ /dev/null @@ -1,21 +0,0 @@ -prices->map(fn(TicketPriceDTO $price) => TicketPriceDomainObject::hydrateFromArray([ - TicketPriceDomainObjectAbstract::PRICE => $ticketsData->type === TicketType::FREE ? 0.00 : $price->price, - TicketPriceDomainObjectAbstract::LABEL => $price->label, - TicketPriceDomainObjectAbstract::SALE_START_DATE => $price->sale_start_date, - TicketPriceDomainObjectAbstract::SALE_END_DATE => $price->sale_end_date, - TicketPriceDomainObjectAbstract::INITIAL_QUANTITY_AVAILABLE => $price->initial_quantity_available, - TicketPriceDomainObjectAbstract::IS_HIDDEN => $price->is_hidden, - ])); - - return $this->ticketCreateService->createTicket( - ticket: (new TicketDomainObject()) - ->setTitle($ticketsData->title) - ->setType($ticketsData->type->name) - ->setOrder($ticketsData->order) - ->setSaleStartDate($ticketsData->sale_start_date) - ->setSaleEndDate($ticketsData->sale_end_date) - ->setMaxPerOrder($ticketsData->max_per_order) - ->setDescription($ticketsData->description) - ->setMinPerOrder($ticketsData->min_per_order) - ->setIsHidden($ticketsData->is_hidden) - ->setHideBeforeSaleStartDate($ticketsData->hide_before_sale_start_date) - ->setHideAfterSaleEndDate($ticketsData->hide_after_sale_end_date) - ->setHideWhenSoldOut($ticketsData->hide_when_sold_out) - ->setStartCollapsed($ticketsData->start_collapsed) - ->setShowQuantityRemaining($ticketsData->show_quantity_remaining) - ->setIsHiddenWithoutPromoCode($ticketsData->is_hidden_without_promo_code) - ->setTicketPrices($ticketPrices) - ->setEventId($ticketsData->event_id), - accountId: $ticketsData->account_id, - taxAndFeeIds: $ticketsData->tax_and_fee_ids, - ); - } -} diff --git a/backend/app/Services/Handlers/Ticket/DTO/UpsertTicketDTO.php b/backend/app/Services/Handlers/Ticket/DTO/UpsertTicketDTO.php deleted file mode 100644 index b19e4d97..00000000 --- a/backend/app/Services/Handlers/Ticket/DTO/UpsertTicketDTO.php +++ /dev/null @@ -1,42 +0,0 @@ -databaseManager->transaction(function () use ($ticketId, $eventId) { - $this->deleteTicket($ticketId, $eventId); - }); - } - - /** - * @throws CannotDeleteEntityException - */ - private function deleteTicket(int $ticketId, int $eventId): void - { - $attendees = $this->attendeeRepository->findWhere( - [ - AttendeeDomainObjectAbstract::EVENT_ID => $eventId, - AttendeeDomainObjectAbstract::TICKET_ID => $ticketId, - ] - ); - - if ($attendees->count() > 0) { - throw new CannotDeleteEntityException( - __('You cannot delete this ticket because it has orders associated with it. You can hide it instead.') - ); - } - - $this->ticketRepository->deleteWhere( - [ - TicketDomainObjectAbstract::EVENT_ID => $eventId, - TicketDomainObjectAbstract::ID => $ticketId, - ] - ); - - $this->ticketPriceRepository->deleteWhere( - [ - TicketPriceDomainObjectAbstract::TICKET_ID => $ticketId, - ] - ); - - $this->logger->info(sprintf('Ticket %d was deleted from event %d', $ticketId, $eventId), [ - 'ticketId' => $ticketId, - 'eventId' => $eventId, - ]); - } -} diff --git a/backend/app/Services/Handlers/Ticket/EditTicketHandler.php b/backend/app/Services/Handlers/Ticket/EditTicketHandler.php deleted file mode 100644 index fb0e8243..00000000 --- a/backend/app/Services/Handlers/Ticket/EditTicketHandler.php +++ /dev/null @@ -1,139 +0,0 @@ -databaseManager->transaction(function () use ($ticketsData) { - $where = [ - 'event_id' => $ticketsData->event_id, - 'id' => $ticketsData->ticket_id, - ]; - - $ticket = $this->updateTicket($ticketsData, $where); - - $this->addTaxes($ticket, $ticketsData); - - $this->priceUpdateService->updatePrices( - $ticket, - $ticketsData, - $ticket->getTicketPrices(), - $this->eventRepository->findById($ticketsData->event_id) - ); - - return $this->ticketRepository - ->loadRelation(TicketPriceDomainObject::class) - ->findById($ticket->getId()); - }); - } - - /** - * @throws CannotChangeTicketTypeException - */ - private function updateTicket(UpsertTicketDTO $ticketsData, array $where): TicketDomainObject - { - $event = $this->eventRepository->findById($ticketsData->event_id); - - $this->validateChangeInTicketType($ticketsData); - - $this->ticketRepository->updateWhere( - attributes: [ - 'title' => $ticketsData->title, - 'type' => $ticketsData->type->name, - 'order' => $ticketsData->order, - 'sale_start_date' => $ticketsData->sale_start_date - ? DateHelper::convertToUTC($ticketsData->sale_start_date, $event->getTimezone()) - : null, - 'sale_end_date' => $ticketsData->sale_end_date - ? DateHelper::convertToUTC($ticketsData->sale_end_date, $event->getTimezone()) - : null, - 'max_per_order' => $ticketsData->max_per_order, - 'description' => $this->purifier->purify($ticketsData->description), - 'min_per_order' => $ticketsData->min_per_order, - 'is_hidden' => $ticketsData->is_hidden, - 'hide_before_sale_start_date' => $ticketsData->hide_before_sale_start_date, - 'hide_after_sale_end_date' => $ticketsData->hide_after_sale_end_date, - 'hide_when_sold_out' => $ticketsData->hide_when_sold_out, - 'start_collapsed' => $ticketsData->start_collapsed, - 'show_quantity_remaining' => $ticketsData->show_quantity_remaining, - 'is_hidden_without_promo_code' => $ticketsData->is_hidden_without_promo_code, - ], - where: $where - ); - - return $this->ticketRepository - ->loadRelation(TicketPriceDomainObject::class) - ->findFirstWhere($where); - } - - /** - * @throws Exception - */ - private function addTaxes(TicketDomainObject $ticket, UpsertTicketDTO $ticketsData): void - { - $this->taxAndTicketAssociationService->addTaxesToTicket( - new TaxAndTicketAssociateParams( - ticketId: $ticket->getId(), - accountId: $ticketsData->account_id, - taxAndFeeIds: $ticketsData->tax_and_fee_ids, - ) - ); - } - - /** - * @throws CannotChangeTicketTypeException - * @todo - We should probably check reserved tickets here as well - */ - private function validateChangeInTicketType(UpsertTicketDTO $ticketsData): void - { - $ticket = $this->ticketRepository - ->loadRelation(TicketPriceDomainObject::class) - ->findById($ticketsData->ticket_id); - - $quantitySold = $ticket->getTicketPrices() - ->sum(fn(TicketPriceDomainObject $price) => $price->getQuantitySold()); - - if ($ticket->getType() !== $ticketsData->type->name && $quantitySold > 0) { - throw new CannotChangeTicketTypeException( - __('Ticket type cannot be changed as tickets have been registered for this type') - ); - } - } -} diff --git a/backend/app/Services/Handlers/Ticket/GetTicketsHandler.php b/backend/app/Services/Handlers/Ticket/GetTicketsHandler.php deleted file mode 100644 index 898e952d..00000000 --- a/backend/app/Services/Handlers/Ticket/GetTicketsHandler.php +++ /dev/null @@ -1,37 +0,0 @@ -ticketRepository - ->loadRelation(TicketPriceDomainObject::class) - ->loadRelation(TaxAndFeesDomainObject::class) - ->findByEventId($eventId, $queryParamsDTO); - - $filteredTickets = $this->ticketFilterService->filter( - tickets: $ticketPaginator->getCollection(), - hideSoldOutTickets: false, - ); - - $ticketPaginator->setCollection($filteredTickets); - - return $ticketPaginator; - } -} diff --git a/backend/app/Services/Handlers/Ticket/SortTicketsHandler.php b/backend/app/Services/Handlers/Ticket/SortTicketsHandler.php deleted file mode 100644 index 25060985..00000000 --- a/backend/app/Services/Handlers/Ticket/SortTicketsHandler.php +++ /dev/null @@ -1,41 +0,0 @@ -sortBy('order')->pluck('id')->toArray(); - - $ticketIdsResult = $this->ticketRepository->findWhere([ - 'event_id' => $eventId, - ]) - ->map(fn($ticket) => $ticket->getId()) - ->toArray(); - - // Check if the orderedTicketIds array exactly matches the ticket IDs from the database - $missingInOrdered = array_diff($ticketIdsResult, $orderedTicketIds); - $extraInOrdered = array_diff($orderedTicketIds, $ticketIdsResult); - - if (!empty($missingInOrdered) || !empty($extraInOrdered)) { - throw new ResourceConflictException( - __('The ordered ticket IDs must exactly match all tickets for the event without missing or extra IDs.') - ); - } - - $this->ticketRepository->sortTickets($eventId, $orderedTicketIds); - } -} diff --git a/backend/app/Validators/CompleteOrderValidator.php b/backend/app/Validators/CompleteOrderValidator.php index 6b8ffc5b..35029f92 100644 --- a/backend/app/Validators/CompleteOrderValidator.php +++ b/backend/app/Validators/CompleteOrderValidator.php @@ -5,23 +5,23 @@ namespace HiEvents\Validators; use HiEvents\DomainObjects\Enums\QuestionBelongsTo; +use HiEvents\DomainObjects\Generated\ProductDomainObjectAbstract; use HiEvents\DomainObjects\Generated\QuestionDomainObjectAbstract; -use HiEvents\DomainObjects\Generated\TicketDomainObjectAbstract; +use HiEvents\DomainObjects\ProductDomainObject; +use HiEvents\DomainObjects\ProductPriceDomainObject; use HiEvents\DomainObjects\QuestionDomainObject; -use HiEvents\DomainObjects\TicketDomainObject; -use HiEvents\DomainObjects\TicketPriceDomainObject; use HiEvents\Repository\Eloquent\Value\Relationship; +use HiEvents\Repository\Interfaces\ProductRepositoryInterface; use HiEvents\Repository\Interfaces\QuestionRepositoryInterface; -use HiEvents\Repository\Interfaces\TicketRepositoryInterface; -use HiEvents\Validators\Rules\AttendeeQuestionRule; use HiEvents\Validators\Rules\OrderQuestionRule; +use HiEvents\Validators\Rules\ProductQuestionRule; use Illuminate\Routing\Route; class CompleteOrderValidator extends BaseValidator { public function __construct( private readonly QuestionRepositoryInterface $questionRepository, - private readonly TicketRepositoryInterface $ticketRepository, + private readonly ProductRepositoryInterface $productRepository, private readonly Route $route ) { @@ -31,8 +31,8 @@ public function rules(): array { $questions = $this->questionRepository ->loadRelation( - new Relationship(TicketDomainObject::class, [ - new Relationship(TicketPriceDomainObject::class) + new Relationship(ProductDomainObject::class, [ + new Relationship(ProductPriceDomainObject::class) ]) ) ->findWhere( @@ -41,33 +41,22 @@ public function rules(): array $orderQuestions = $questions->filter( fn(QuestionDomainObject $question) => $question->getBelongsTo() === QuestionBelongsTo::ORDER->name ); - $ticketQuestions = $questions->filter( - fn(QuestionDomainObject $question) => $question->getBelongsTo() === QuestionBelongsTo::TICKET->name + $productQuestions = $questions->filter( + fn(QuestionDomainObject $question) => $question->getBelongsTo() === QuestionBelongsTo::PRODUCT->name ); - $tickets = $this->ticketRepository - ->loadRelation(TicketPriceDomainObject::class) + $products = $this->productRepository + ->loadRelation(ProductPriceDomainObject::class) ->findWhere( - [TicketDomainObjectAbstract::EVENT_ID => $this->route->parameter('event_id')] + [ProductDomainObjectAbstract::EVENT_ID => $this->route->parameter('event_id')] ); return [ 'order.first_name' => ['required', 'string', 'max:40'], 'order.last_name' => ['required', 'string', 'max:40'], - 'order.questions' => new OrderQuestionRule($orderQuestions, $tickets), + 'order.questions' => new OrderQuestionRule($orderQuestions, $products), 'order.email' => 'required|email', - 'attendees.*.first_name' => ['required', 'string', 'max:40'], - 'attendees.*.last_name' => ['required', 'string', 'max:40'], - 'attendees.*.email' => ['required', 'email'], - 'attendees' => new AttendeeQuestionRule($ticketQuestions, $tickets), - - // Address validation is intentionally not strict, as we want to support all countries - 'order.address.address_line_1' => ['string', 'max:255'], - 'order.address.address_line_2' => ['string', 'max:255', 'nullable'], - 'order.address.city' => ['string', 'max:85'], - 'order.address.state_or_region' => ['string', 'max:85'], - 'order.address.zip_or_postal_code' => ['string', 'max:85'], - 'order.address.country' => ['string', 'max:2'], + 'products' => new ProductQuestionRule($productQuestions, $products), ]; } @@ -77,9 +66,6 @@ public function messages(): array 'order.first_name' => __('First name is required'), 'order.last_name' => __('Last name is required'), 'order.email' => __('A valid email is required'), - 'attendees.*.first_name' => __('First name is required'), - 'attendees.*.last_name' => __('Last name is required'), - 'attendees.*.email' => __('A valid email is required'), ]; } } diff --git a/backend/app/Validators/Rules/BaseQuestionRule.php b/backend/app/Validators/Rules/BaseQuestionRule.php index 7571bf13..f30ce6e0 100644 --- a/backend/app/Validators/Rules/BaseQuestionRule.php +++ b/backend/app/Validators/Rules/BaseQuestionRule.php @@ -5,8 +5,8 @@ use Closure; use HiEvents\DomainObjects\Enums\QuestionTypeEnum; use HiEvents\DomainObjects\QuestionDomainObject; -use HiEvents\DomainObjects\TicketDomainObject; -use HiEvents\DomainObjects\TicketPriceDomainObject; +use HiEvents\DomainObjects\ProductDomainObject; +use HiEvents\DomainObjects\ProductPriceDomainObject; use Illuminate\Contracts\Validation\DataAwareRule; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidatorAwareRule; @@ -34,7 +34,7 @@ abstract class BaseQuestionRule implements ValidationRule, DataAwareRule, Valida protected Collection $questions; - private Collection $tickets; + private Collection $products; protected Validator $validator; @@ -44,20 +44,20 @@ abstract protected function validateRequiredQuestionArePresent(Collection $data) abstract protected function validateQuestions(mixed $data): array; - public function __construct(Collection $questions, Collection $tickets) + public function __construct(Collection $questions, Collection $products) { $this->questions = $questions; - $this->tickets = $tickets; + $this->products = $products; } public function validate(string $attribute, mixed $value, Closure $fail): void { $this->validateRequiredQuestionArePresent(collect($value)); - $validationMessages = $this->validateQuestions($value); + $questionValidationMessages = $this->validateQuestions($value); - if ($validationMessages) { - $this->validator->messages()->merge($validationMessages); + if ($questionValidationMessages) { + $this->validator->messages()->merge($questionValidationMessages); } } @@ -73,21 +73,21 @@ public function setData(array $data): void $this->data = $data; } - protected function getTicketIdFromTicketPriceId(int $ticketPriceId): int + protected function getProductIdFromProductPriceId(int $productPriceId): int { - $ticketPrices = new Collection(); - $this->tickets->each(fn(TicketDomainObject $ticket) => $ticketPrices->push(...$ticket->getTicketPrices())); + $productPrices = new Collection(); + $this->products->each(fn(ProductDomainObject $product) => $productPrices->push(...$product->getProductPrices())); - /** @var TicketPriceDomainObject $ticketPrice */ - $ticketPrice = $ticketPrices - ->first(fn(TicketPriceDomainObject $ticketPrice) => $ticketPrice->getId() === $ticketPriceId); + /** @var ProductPriceDomainObject $productPrice */ + $productPrice = $productPrices + ->first(fn(ProductPriceDomainObject $productPrice) => $productPrice->getId() === $productPriceId); - return $ticketPrice->getTicketId(); + return $productPrice->getProductId(); } protected function isAnswerValid(QuestionDomainObject $questionDomainObject, mixed $response): bool { - if (!$questionDomainObject->isMultipleChoice()) { + if (!$questionDomainObject->isPreDefinedChoice()) { return true; } @@ -96,7 +96,7 @@ protected function isAnswerValid(QuestionDomainObject $questionDomainObject, mix } if (is_string($response['answer'])) { - return in_array($response, $questionDomainObject->getOptions(), true); + return in_array($response['answer'], $questionDomainObject->getOptions(), true); } return array_diff((array)$response['answer'], $questionDomainObject->getOptions()) === []; @@ -160,4 +160,9 @@ protected function validateResponseLength( return $validationMessages; } + + protected function getProductDomainObject(int $id): ?ProductDomainObject + { + return $this->products->filter(fn($product) => $product->getId() === $id)?->first(); + } } diff --git a/backend/app/Validators/Rules/AttendeeQuestionRule.php b/backend/app/Validators/Rules/ProductQuestionRule.php similarity index 50% rename from backend/app/Validators/Rules/AttendeeQuestionRule.php rename to backend/app/Validators/Rules/ProductQuestionRule.php index 87640c9e..534dd92e 100644 --- a/backend/app/Validators/Rules/AttendeeQuestionRule.php +++ b/backend/app/Validators/Rules/ProductQuestionRule.php @@ -2,26 +2,28 @@ namespace HiEvents\Validators\Rules; +use HiEvents\DomainObjects\Enums\ProductType; use HiEvents\DomainObjects\QuestionDomainObject; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Validator; use Illuminate\Validation\ValidationException; -class AttendeeQuestionRule extends BaseQuestionRule +class ProductQuestionRule extends BaseQuestionRule { /** * @throws ValidationException */ - protected function validateRequiredQuestionArePresent(Collection $orderAttendees): void + protected function validateRequiredQuestionArePresent(Collection $orderProducts): void { - foreach ($orderAttendees as $attendee) { - $ticketId = $this->getTicketIdFromTicketPriceId($attendee['ticket_price_id']); - $questions = $attendee['questions'] ?? []; + foreach ($orderProducts as $productData) { + $productId = $this->getProductIdFromProductPriceId($productData['product_price_id']); + $questions = $productData['questions'] ?? []; $requiredQuestionIds = $this->questions - ->filter(function (QuestionDomainObject $question) use ($ticketId) { + ->filter(function (QuestionDomainObject $question) use ($productId) { return $question->getRequired() && !$question->getIsHidden() - && $question->getTickets()?->map(fn($ticket) => $ticket->getId())->contains($ticketId); + && $question->getProducts()?->map(fn($product) => $product->getId())->contains($productId); }) ->map(fn(QuestionDomainObject $question) => $question->getId()); @@ -33,15 +35,29 @@ protected function validateRequiredQuestionArePresent(Collection $orderAttendees } } - protected function validateQuestions(mixed $attendees): array + protected function validateQuestions(mixed $products): array { $validationMessages = []; - foreach ($attendees as $attendeeIndex => $attendee) { - $questions = $attendee['questions'] ?? []; + foreach ($products as $productIndex => $productRequestData) { + $productDomainObject = $this->getProductDomainObject($productRequestData['product_id']); + + if (!$productDomainObject) { + $validationMessages['products.' . $productIndex][] = __('This product is outdated. Please reload the page.'); + continue; + } + + if ($productDomainObject->getProductType() === ProductType::TICKET->name) { + $validationMessages = [ + ...$validationMessages, + ...$this->validateBasicTicketFields($productRequestData, $productIndex), + ]; + } + + $questions = $productRequestData['questions'] ?? []; foreach ($questions as $questionIndex => $question) { $questionDomainObject = $this->getQuestionDomainObject($question['question_id'] ?? null); - $key = 'attendees.' . $attendeeIndex . '.questions.' . $questionIndex . '.response'; + $key = 'products.' . $productIndex . '.questions.' . $questionIndex . '.response'; $response = empty($question['response']) ? null : $question['response']; if (!$questionDomainObject) { @@ -67,4 +83,25 @@ protected function validateQuestions(mixed $attendees): array return $validationMessages; } + + private function validateBasicTicketFields(mixed $productRequestData, int|string $productIndex): array + { + $validationMessages = []; + + $validator = Validator::make($productRequestData, [ + 'first_name' => ['required', 'string', 'min:1', 'max:100'], + 'last_name' => ['required', 'string', 'min:1', 'max:100'], + 'email' => ['required', 'string', 'email', 'max:100'], + ]); + + if ($validator->fails()) { + foreach ($validator->errors()->messages() as $field => $messages) { + foreach ($messages as $message) { + $validationMessages["products.$productIndex.$field"][] = $message; + } + } + } + + return $validationMessages; + } } diff --git a/backend/app/Validators/Rules/RulesHelper.php b/backend/app/Validators/Rules/RulesHelper.php index 20826cfe..a68729c2 100644 --- a/backend/app/Validators/Rules/RulesHelper.php +++ b/backend/app/Validators/Rules/RulesHelper.php @@ -16,4 +16,5 @@ class RulesHelper public const REQUIRED_EMAIL = ['email' , 'required', 'max:100']; + public const OPTIONAL_TEXT_MEDIUM_LENGTH = ['string', 'max:2000', 'nullable']; } diff --git a/backend/config/app.php b/backend/config/app.php index 04ac89ac..86288b80 100644 --- a/backend/config/app.php +++ b/backend/config/app.php @@ -24,12 +24,12 @@ 'homepage_views_update_batch_size' => env('APP_HOMEPAGE_VIEWS_UPDATE_BATCH_SIZE', 8), /** - * The number of seconds to cache the ticket quantities on the homepage + * The number of seconds to cache the product quantities on the homepage * It is recommended to cache this value for a short period of time for high traffic sites * * Set to null to disable caching */ - 'homepage_ticket_quantities_cache_ttl' => env('APP_HOMEPAGE_TICKET_QUANTITIES_CACHE_TTL', 2), + 'homepage_product_quantities_cache_ttl' => env('APP_HOMEPAGE_TICKET_QUANTITIES_CACHE_TTL', 2), 'frontend_urls' => [ 'confirm_email_address' => '/manage/profile/confirm-email-address/%s', @@ -39,7 +39,7 @@ 'stripe_connect_return_url' => '/account/payment', 'stripe_connect_refresh_url' => '/account/payment', 'event_homepage' => '/event/%d/%s', - 'attendee_ticket' => '/ticket/%d/%s', + 'attendee_product' => '/product/%d/%s', 'order_summary' => '/checkout/%d/%s/summary', 'organizer_order_summary' => '/manage/event/%d/orders#order-%d', ], diff --git a/backend/config/auth.php b/backend/config/auth.php index 17cb955c..80e7922d 100644 --- a/backend/config/auth.php +++ b/backend/config/auth.php @@ -44,6 +44,10 @@ 'driver' => 'jwt', 'provider' => 'users', ], + 'sanctum' => [ + 'driver' => 'sanctum', // For Sanctum + 'provider' => 'users', + ], ], /* diff --git a/backend/config/sanctum.php b/backend/config/sanctum.php index 00096f88..c40fd010 100644 --- a/backend/config/sanctum.php +++ b/backend/config/sanctum.php @@ -33,7 +33,7 @@ | */ - 'guard' => ['web'], + 'guard' => ['api'], /* |-------------------------------------------------------------------------- diff --git a/backend/database/migrations/2024_09_20_032323_rename_tickets_to_products.php b/backend/database/migrations/2024_09_20_032323_rename_tickets_to_products.php new file mode 100644 index 00000000..907f3fab --- /dev/null +++ b/backend/database/migrations/2024_09_20_032323_rename_tickets_to_products.php @@ -0,0 +1,182 @@ +renameColumn('ticket_id', 'product_id'); + $table->renameColumn('ticket_price_id', 'product_price_id'); + }); + + Schema::table('attendees', function (Blueprint $table) { + $table->renameColumn('ticket_id', 'product_id'); + $table->renameColumn('ticket_price_id', 'product_price_id'); + }); + + Schema::table('product_prices', function (Blueprint $table) { + $table->renameColumn('ticket_id', 'product_id'); + }); + + Schema::table('product_taxes_and_fees', function (Blueprint $table) { + $table->renameColumn('ticket_id', 'product_id'); + }); + + Schema::table('product_questions', function (Blueprint $table) { + $table->renameColumn('ticket_id', 'product_id'); + }); + + Schema::table('product_check_in_lists', function (Blueprint $table) { + $table->renameColumn('ticket_id', 'product_id'); + }); + + Schema::table('attendee_check_ins', function (Blueprint $table) { + $table->renameColumn('ticket_id', 'product_id'); + }); + + Schema::table('product_capacity_assignments', function (Blueprint $table) { + $table->renameColumn('ticket_id', 'product_id'); + }); + + Schema::table('question_answers', function (Blueprint $table) { + $table->renameColumn('ticket_id', 'product_id'); + }); + + Schema::table('promo_codes', function (Blueprint $table) { + $table->renameColumn('applicable_ticket_ids', 'applicable_product_ids'); + }); + + Schema::table('event_statistics', function (Blueprint $table) { + $table->renameColumn('tickets_sold', 'products_sold'); + }); + + Schema::table('event_daily_statistics', function (Blueprint $table) { + $table->renameColumn('tickets_sold', 'products_sold'); + }); + + Schema::table('messages', function (Blueprint $table) { + $table->renameColumn('ticket_ids', 'product_ids'); + }); + + Schema::table('event_settings', function (Blueprint $table) { + $table->renameColumn('ticket_page_message', 'product_page_message'); + }); + + $this->renameIndex('idx_ticket_prices_ticket_id', 'idx_product_prices_product_id'); + $this->renameIndex('order_items_ticket_id_index', 'order_items_product_id_index'); + $this->renameIndex('order_items_ticket_price_id_index', 'order_items_product_price_id_index'); + $this->renameIndex('idx_attendees_ticket_id_deleted_at', 'idx_attendees_product_id_deleted_at'); + $this->renameIndex('ticket_tax_and_fees_ticket_id_index', 'product_tax_and_fees_product_id_index'); + $this->renameIndex('idx_ticket_questions_active', 'idx_product_questions_active'); + $this->renameIndex('ticket_check_in_lists_ticket_id_check_in_list_id_index', 'product_check_in_lists_product_id_check_in_list_id_index'); + $this->renameIndex('idx_ticket_check_in_lists_ticket_id_deleted_at', 'idx_product_check_in_lists_product_id_deleted_at'); + $this->renameIndex('attendee_check_ins_ticket_id_index', 'attendee_check_ins_product_id_index'); + $this->renameIndex('ticket_capacity_assignments_ticket_id_index', 'product_capacity_assignments_product_id_index'); + $this->renameIndex('attendees_ticket_prices_id_fk', 'attendees_product_prices_id_fk'); + } + + public function down(): void + { + Schema::rename('products', 'tickets'); + + Schema::rename('product_prices', 'ticket_prices'); + Schema::rename('product_taxes_and_fees', 'ticket_taxes_and_fees'); + Schema::rename('product_questions', 'ticket_questions'); + Schema::rename('product_check_in_lists', 'ticket_check_in_lists'); + Schema::rename('product_capacity_assignments', 'ticket_capacity_assignments'); + + // Rename sequences back + DB::statement('ALTER SEQUENCE product_capacity_assignments_id_seq RENAME TO ticket_capacity_assignments_id_seq'); + DB::statement('ALTER SEQUENCE product_check_in_lists_id_seq RENAME TO ticket_check_in_lists_id_seq'); + + Schema::table('order_items', function (Blueprint $table) { + $table->renameColumn('product_id', 'ticket_id'); + $table->renameColumn('product_price_id', 'ticket_price_id'); + }); + + Schema::table('attendees', function (Blueprint $table) { + $table->renameColumn('product_id', 'ticket_id'); + $table->renameColumn('product_price_id', 'ticket_price_id'); + }); + + Schema::table('ticket_prices', function (Blueprint $table) { + $table->renameColumn('product_id', 'ticket_id'); + }); + + Schema::table('ticket_taxes_and_fees', function (Blueprint $table) { + $table->renameColumn('product_id', 'ticket_id'); + }); + + Schema::table('ticket_questions', function (Blueprint $table) { + $table->renameColumn('product_id', 'ticket_id'); + }); + + Schema::table('ticket_check_in_lists', function (Blueprint $table) { + $table->renameColumn('product_id', 'ticket_id'); + }); + + Schema::table('attendee_check_ins', function (Blueprint $table) { + $table->renameColumn('product_id', 'ticket_id'); + }); + + Schema::table('ticket_capacity_assignments', function (Blueprint $table) { + $table->renameColumn('product_id', 'ticket_id'); + }); + + Schema::table('question_answers', function (Blueprint $table) { + $table->renameColumn('product_id', 'ticket_id'); + }); + + Schema::table('promo_codes', function (Blueprint $table) { + $table->renameColumn('applicable_product_ids', 'applicable_ticket_ids'); + }); + + Schema::table('event_statistics', function (Blueprint $table) { + $table->renameColumn('products_sold', 'tickets_sold'); + }); + + Schema::table('event_daily_statistics', function (Blueprint $table) { + $table->renameColumn('products_sold', 'tickets_sold'); + }); + + Schema::table('messages', function (Blueprint $table) { + $table->renameColumn('product_ids', 'ticket_ids'); + }); + + Schema::table('event_settings', function (Blueprint $table) { + $table->renameColumn('product_page_message', 'ticket_page_message'); + }); + + $this->renameIndex('idx_product_prices_product_id', 'idx_ticket_prices_ticket_id'); + $this->renameIndex('order_items_product_id_index', 'order_items_ticket_id_index'); + $this->renameIndex('order_items_product_price_id_index', 'order_items_ticket_price_id_index'); + $this->renameIndex('idx_attendees_product_id_deleted_at', 'idx_attendees_ticket_id_deleted_at'); + $this->renameIndex('product_tax_and_fees_product_id_index', 'ticket_tax_and_fees_ticket_id_index'); + $this->renameIndex('idx_product_questions_active', 'idx_ticket_questions_active'); + $this->renameIndex('product_check_in_lists_product_id_check_in_list_id_index', 'ticket_check_in_lists_ticket_id_check_in_list_id_index'); + $this->renameIndex('idx_product_check_in_lists_product_id_deleted_at', 'idx_ticket_check_in_lists_ticket_id_deleted_at'); + $this->renameIndex('attendee_check_ins_product_id_index', 'attendee_check_ins_ticket_id_index'); + $this->renameIndex('product_capacity_assignments_product_id_index', 'ticket_capacity_assignments_ticket_id_index'); + $this->renameIndex('attendees_product_prices_id_fk', 'attendees_ticket_prices_id_fk'); + } + + private function renameIndex($from, $to): void + { + DB::statement("ALTER INDEX IF EXISTS {$from} RENAME TO {$to}"); + } +}; diff --git a/backend/database/migrations/2024_09_20_032838_add_product_type_to_products.php b/backend/database/migrations/2024_09_20_032838_add_product_type_to_products.php new file mode 100644 index 00000000..fd8974b7 --- /dev/null +++ b/backend/database/migrations/2024_09_20_032838_add_product_type_to_products.php @@ -0,0 +1,24 @@ +enum('product_type', ProductType::valuesArray()) + ->default(ProductType::TICKET->name) + ->after('id'); + }); + } + + public function down(): void + { + Schema::table('products', static function (Blueprint $table) { + $table->dropColumn('product_type'); + }); + } +}; diff --git a/backend/database/migrations/2024_09_23_032009_add_product_categories_table.php b/backend/database/migrations/2024_09_23_032009_add_product_categories_table.php new file mode 100644 index 00000000..5a4b2f88 --- /dev/null +++ b/backend/database/migrations/2024_09_23_032009_add_product_categories_table.php @@ -0,0 +1,68 @@ +id(); + $table->string('name'); + $table->string('no_products_message')->nullable(); + $table->string('description')->nullable(); + $table->boolean('is_hidden')->default(false); + $table->tinyInteger('order')->default(0); + + $table->unsignedBigInteger('event_id'); + $table->foreign('event_id')->references('id')->on('events')->onDelete('cascade'); + + $table->timestamps(); + $table->softDeletes(); + + $table->index('event_id'); + $table->index('is_hidden'); + $table->index('order'); + }); + + Schema::table('products', static function (Blueprint $table) { + $table->unsignedBigInteger('product_category_id')->nullable(); + $table->foreign('product_category_id')->references('id')->on('product_categories')->onDelete('set null'); + }); + + $events = DB::table('events')->get(); + + foreach ($events as $event) { + $categoryId = DB::table('product_categories')->insertGetId([ + 'name' => __('Tickets'), + 'event_id' => $event->id, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('products') + ->where('event_id', $event->id) + ->update(['product_category_id' => $categoryId]); + } + + DB::table('questions') + ->where('belongs_to', 'TICKET') + ->update(['belongs_to' => 'PRODUCT']); + } + + public function down(): void + { + Schema::table('products', static function (Blueprint $table) { + $table->dropForeign(['product_category_id']); + $table->dropColumn('product_category_id'); + }); + + Schema::dropIfExists('product_categories'); + + DB::table('questions') + ->where('belongs_to', 'PRODUCT') + ->update(['belongs_to' => 'TICKET']); + } +}; diff --git a/backend/database/migrations/2024_09_29_053757_add_product_type_to_order_items_table.php b/backend/database/migrations/2024_09_29_053757_add_product_type_to_order_items_table.php new file mode 100644 index 00000000..79f7a26e --- /dev/null +++ b/backend/database/migrations/2024_09_29_053757_add_product_type_to_order_items_table.php @@ -0,0 +1,22 @@ +string('product_type')->default(ProductType::TICKET->name); + }); + } + + public function down(): void + { + Schema::table('order_items', function (Blueprint $table) { + $table->dropColumn('product_type'); + }); + } +}; diff --git a/backend/database/migrations/2024_10_01_003655_update_question_and_answer_views_view.php b/backend/database/migrations/2024_10_01_003655_update_question_and_answer_views_view.php new file mode 100644 index 00000000..4f0db80e --- /dev/null +++ b/backend/database/migrations/2024_10_01_003655_update_question_and_answer_views_view.php @@ -0,0 +1,54 @@ +unsignedInteger('attendees_registered')->default(0); + }); + + DB::statement('UPDATE event_statistics SET attendees_registered = products_sold'); + } + + if (!Schema::hasColumn('event_daily_statistics', 'attendees_registered')) { + Schema::table('event_daily_statistics', static function (Blueprint $table) { + $table->unsignedInteger('attendees_registered')->default(0); + }); + + DB::statement('UPDATE event_daily_statistics SET attendees_registered = products_sold'); + } + } + + public function down(): void + { + if (Schema::hasColumn('event_statistics', 'attendees_registered')) { + Schema::table('event_statistics', static function (Blueprint $table) { + $table->dropColumn('attendees_registered'); + }); + } + + if (Schema::hasColumn('event_daily_statistics', 'attendees_registered')) { + Schema::table('event_daily_statistics', static function (Blueprint $table) { + $table->dropColumn('attendees_registered'); + }); + } + } +}; diff --git a/backend/database/migrations/2024_10_14_232118_add_start_collapsed_to_tickets.php b/backend/database/migrations/2024_10_14_232118_add_start_collapsed_to_tickets.php index a453959c..480a9fe3 100644 --- a/backend/database/migrations/2024_10_14_232118_add_start_collapsed_to_tickets.php +++ b/backend/database/migrations/2024_10_14_232118_add_start_collapsed_to_tickets.php @@ -2,21 +2,25 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { public function up(): void { - Schema::table('tickets', static function (Blueprint $table) { - $table->boolean('start_collapsed')->default(false); - }); + $table = Schema::hasTable('tickets') ? 'tickets' : 'products'; + + if (!Schema::hasColumn($table, 'start_collapsed')) { + Schema::table($table, static function (Blueprint $table) { + $table->boolean('start_collapsed')->default(false); + }); + } } public function down(): void { - Schema::table('tickets', static function (Blueprint $table) { + $table = Schema::hasTable('tickets') ? 'tickets' : 'products'; + + Schema::table($table, static function (Blueprint $table) { $table->dropColumn('start_collapsed'); }); } diff --git a/backend/database/migrations/2024_11_22_235559_update_capacity_assignment_applies_to.php b/backend/database/migrations/2024_11_22_235559_update_capacity_assignment_applies_to.php new file mode 100644 index 00000000..2e1293ea --- /dev/null +++ b/backend/database/migrations/2024_11_22_235559_update_capacity_assignment_applies_to.php @@ -0,0 +1,20 @@ +text('notes')->nullable(); + }); + } + + public function down(): void + { + Schema::table('attendees', static function (Blueprint $table) { + if (!Schema::hasColumn('attendees', 'notes')) { + return; + } + $table->dropColumn('notes'); + }); + } +}; diff --git a/backend/database/migrations/2025_01_16_161633_add_account_id_to_access_tokens.php b/backend/database/migrations/2025_01_16_161633_add_account_id_to_access_tokens.php new file mode 100644 index 00000000..26f9df35 --- /dev/null +++ b/backend/database/migrations/2025_01_16_161633_add_account_id_to_access_tokens.php @@ -0,0 +1,25 @@ +foreignId('account_id') + ->constrained() + ->onDelete('cascade'); + }); + } + + public function down(): void + { + Schema::table('personal_access_tokens', static function (Blueprint $table) { + $table->dropColumn('account_id'); + }); + } +}; \ No newline at end of file diff --git a/backend/lang/de.json b/backend/lang/de.json index 318fe6a9..66338ae2 100644 --- a/backend/lang/de.json +++ b/backend/lang/de.json @@ -283,5 +283,53 @@ "Attendee does not belong to this check-in list": "Teilnehmer gehört nicht zu dieser Eincheckliste", "Attendee :attendee_name\\'s ticket is cancelled": "Das Ticket von Teilnehmer :attendee_name wurde storniert", "Check-in list is not active yet": "Die Check-in-Liste ist noch nicht aktiv", - "The number of attendees does not match the number of tickets in the order": "" -} \ No newline at end of file + "The number of attendees does not match the number of tickets in the order": "Die Anzahl der Teilnehmer stimmt nicht mit der Anzahl der Tickets in der Bestellung überein.", + "Product is required": "Produkt ist erforderlich.", + "Product price is required": "Produktpreis ist erforderlich.", + "Please select at least one product.": "Bitte wählen Sie mindestens ein Produkt aus.", + "The sale start date must be after the product sale start date.": "Das Verkaufsstartdatum muss nach dem Verkaufsstartdatum des Produkts liegen.", + "You must select a product category.": "Sie müssen eine Produktkategorie auswählen.", + "Invalid direction. Must be either asc or desc": "Ungültige Richtung. Muss entweder asc oder desc sein.", + "DomainObject must be a valid :interface.": "DomainObject muss eine gültige :interface sein.", + "Nested relationships must be an array of Relationship objects.": "Verschachtelte Beziehungen müssen ein Array von Relationship-Objekten sein.", + "OrderAndDirections must be an array of OrderAndDirection objects.": "OrderAndDirections muss ein Array von OrderAndDirection-Objekten sein.", + "Attendee :attendee_name\\'s product is cancelled": "Das Produkt von Teilnehmer :attendee_name wurde storniert.", + "Tickets": "Tickets", + "There are no tickets available for this event.": "Es sind keine Tickets für diese Veranstaltung verfügbar.", + "You haven\\'t selected any products": "Sie haben keine Produkte ausgewählt.", + "The maximum number of products available for :products is :max": "Die maximale Anzahl der verfügbaren Produkte für :products beträgt :max.", + "You must order at least :min products for :product": "Sie müssen mindestens :min Produkte für :product bestellen.", + "The product :product is sold out": "Das Produkt :product ist ausverkauft.", + "The maximum number of products available for :product is :max": "Die maximale Anzahl der verfügbaren Produkte für :product beträgt :max.", + "Sorry, these products are sold out": "Entschuldigung, diese Produkte sind ausverkauft.", + "The maximum number of products available is :max": "Die maximale Anzahl der verfügbaren Produkte beträgt :max.", + "Product with id :id not found": "Produkt mit ID :id nicht gefunden.", + "You cannot delete this product because it has orders associated with it. You can hide it instead.": "Sie können dieses Produkt nicht löschen, da damit Bestellungen verknüpft sind. Sie können es stattdessen ausblenden.", + "Invalid product ids: :ids": "Ungültige Produkt-IDs: :ids.", + "Product is hidden without promo code": "Produkt ist ohne Aktionscode verborgen.", + "Product is sold out": "Produkt ist ausverkauft.", + "Product is before sale start date": "Produkt ist vor dem Verkaufsstartdatum.", + "Product is after sale end date": "Produkt ist nach dem Verkaufsendedatum.", + "Product is hidden": "Produkt ist verborgen.", + "Cannot delete product price with id :id because it has sales": "Produktpreis mit ID :id kann nicht gelöscht werden, da es Verkäufe gibt.", + "You cannot delete this product category because it contains the following products: :products. These products are linked to existing orders. Please move the :product_name to another category before attempting to delete this one.": "Sie können diese Produktkategorie nicht löschen, da sie die folgenden Produkte enthält: :products. Diese Produkte sind mit bestehenden Bestellungen verknüpft. Bitte verschieben Sie :product_name in eine andere Kategorie, bevor Sie versuchen, diese zu löschen.", + "products": "Produkte", + "product": "Produkt", + "Product category :productCategoryId has been deleted.": "Produktkategorie :productCategoryId wurde gelöscht.", + "You cannot delete the last product category. Please create another category before deleting this one.": "Sie können die letzte Produktkategorie nicht löschen. Bitte erstellen Sie eine weitere Kategorie, bevor Sie diese löschen.", + "The product category with ID :id was not found.": "Die Produktkategorie mit der ID :id wurde nicht gefunden.", + "Expired": "Abgelaufen", + "Limit Reached": "Limit erreicht", + "Deleted": "Gelöscht", + "Active": "Aktiv", + "This ticket is invalid": "Dieses Ticket ist ungültig.", + "There are no tickets available. ' .\n 'If you would like to assign a product to this attendee,' .\n ' please adjust the product\\'s available quantity.": "Es sind keine Tickets verfügbar. Wenn Sie einem Teilnehmer ein Produkt zuweisen möchten, passen Sie bitte die verfügbare Menge des Produkts an.", + "The product price ID is invalid.": "Die Produktpreis-ID ist ungültig.", + "Product ID is not valid": "Produkt-ID ist ungültig.", + "There are no products available. If you would like to assign this product to this attendee, please adjust the product\\'s available quantity.": "Es sind keine Produkte verfügbar. Wenn Sie diesem Teilnehmer ein Produkt zuweisen möchten, passen Sie bitte die verfügbare Menge des Produkts an.", + "There is an unexpected product price ID in the order": "Es gibt eine unerwartete Produktpreis-ID in der Bestellung.", + "Product type cannot be changed as products have been registered for this type": "Der Produkttyp kann nicht geändert werden, da Produkte für diesen Typ registriert wurden.", + "The ordered category IDs must exactly match all categories for the event without missing or extra IDs.": "Die bestellten Kategorie-IDs müssen genau mit allen Kategorien der Veranstaltung übereinstimmen, ohne fehlende oder zusätzliche IDs.", + "The ordered product IDs must exactly match all products for the event without missing or extra IDs.": "Die bestellten Produkt-IDs müssen genau mit allen Produkten der Veranstaltung übereinstimmen, ohne fehlende oder zusätzliche IDs.", + "This product is outdated. Please reload the page.": "Dieses Produkt ist veraltet. Bitte laden Sie die Seite neu." +} diff --git a/backend/lang/es.json b/backend/lang/es.json index 216230a4..c55bc7cb 100644 --- a/backend/lang/es.json +++ b/backend/lang/es.json @@ -283,5 +283,53 @@ "Attendee does not belong to this check-in list": "El asistente no pertenece a esta lista de registro", "Attendee :attendee_name\\'s ticket is cancelled": "La entrada del asistente :attendee_name ha sido cancelada", "Check-in list is not active yet": "La lista de registro aún no está activa", - "The number of attendees does not match the number of tickets in the order": "" -} \ No newline at end of file + "The number of attendees does not match the number of tickets in the order": "El número de asistentes no coincide con el número de entradas en el pedido.", + "Product is required": "El producto es obligatorio.", + "Product price is required": "El precio del producto es obligatorio.", + "Please select at least one product.": "Por favor, selecciona al menos un producto.", + "The sale start date must be after the product sale start date.": "La fecha de inicio de la venta debe ser posterior a la fecha de inicio de la venta del producto.", + "You must select a product category.": "Debes seleccionar una categoría de producto.", + "Invalid direction. Must be either asc or desc": "Dirección inválida. Debe ser asc o desc.", + "DomainObject must be a valid :interface.": "El objeto de dominio debe ser una :interface válida.", + "Nested relationships must be an array of Relationship objects.": "Las relaciones anidadas deben ser un array de objetos de relación.", + "OrderAndDirections must be an array of OrderAndDirection objects.": "OrderAndDirections debe ser un array de objetos OrderAndDirection.", + "Attendee :attendee_name\\'s product is cancelled": "El producto del asistente :attendee_name está cancelado.", + "Tickets": "Entradas", + "There are no tickets available for this event.": "No hay entradas disponibles para este evento.", + "You haven\\'t selected any products": "No has seleccionado ningún producto.", + "The maximum number of products available for :products is :max": "El número máximo de productos disponibles para :products es :max.", + "You must order at least :min products for :product": "Debes pedir al menos :min productos para :product.", + "The product :product is sold out": "El producto :product está agotado.", + "The maximum number of products available for :product is :max": "El número máximo de productos disponibles para :product es :max.", + "Sorry, these products are sold out": "Lo sentimos, estos productos están agotados.", + "The maximum number of products available is :max": "El número máximo de productos disponibles es :max.", + "Product with id :id not found": "Producto con id :id no encontrado.", + "You cannot delete this product because it has orders associated with it. You can hide it instead.": "No puedes eliminar este producto porque tiene pedidos asociados. Puedes ocultarlo en su lugar.", + "Invalid product ids: :ids": "IDs de productos inválidos: :ids.", + "Product is hidden without promo code": "El producto está oculto sin código promocional.", + "Product is sold out": "El producto está agotado.", + "Product is before sale start date": "El producto está antes de la fecha de inicio de la venta.", + "Product is after sale end date": "El producto está después de la fecha de finalización de la venta.", + "Product is hidden": "El producto está oculto.", + "Cannot delete product price with id :id because it has sales": "No se puede eliminar el precio del producto con id :id porque tiene ventas.", + "You cannot delete this product category because it contains the following products: :products. These products are linked to existing orders. Please move the :product_name to another category before attempting to delete this one.": "No puedes eliminar esta categoría de producto porque contiene los siguientes productos: :products. Estos productos están vinculados a pedidos existentes. Mueve el :product_name a otra categoría antes de intentar eliminar esta.", + "products": "productos", + "product": "producto", + "Product category :productCategoryId has been deleted.": "La categoría de producto :productCategoryId ha sido eliminada.", + "You cannot delete the last product category. Please create another category before deleting this one.": "No puedes eliminar la última categoría de producto. Por favor, crea otra categoría antes de eliminar esta.", + "The product category with ID :id was not found.": "No se encontró la categoría de producto con ID :id.", + "Expired": "Expirado", + "Limit Reached": "Límite alcanzado", + "Deleted": "Eliminado", + "Active": "Activo", + "This ticket is invalid": "Este ticket no es válido.", + "There are no tickets available. ' .\n 'If you would like to assign a product to this attendee,' .\n ' please adjust the product\\'s available quantity.": "No hay entradas disponibles. Si deseas asignar un producto a este asistente, ajusta la cantidad disponible del producto.", + "The product price ID is invalid.": "El ID de precio del producto no es válido.", + "Product ID is not valid": "El ID del producto no es válido.", + "There are no products available. If you would like to assign this product to this attendee, please adjust the product\\'s available quantity.": "No hay productos disponibles. Si deseas asignar este producto a este asistente, ajusta la cantidad disponible del producto.", + "There is an unexpected product price ID in the order": "Hay un ID de precio de producto inesperado en el pedido.", + "Product type cannot be changed as products have been registered for this type": "El tipo de producto no se puede cambiar porque se han registrado productos para este tipo.", + "The ordered category IDs must exactly match all categories for the event without missing or extra IDs.": "Los IDs de categoría ordenados deben coincidir exactamente con todas las categorías del evento sin faltar ni sobrar IDs.", + "The ordered product IDs must exactly match all products for the event without missing or extra IDs.": "Los IDs de producto ordenados deben coincidir exactamente con todos los productos del evento sin faltar ni sobrar IDs.", + "This product is outdated. Please reload the page.": "Este producto está desactualizado. Por favor, recarga la página." +} diff --git a/backend/lang/fr.json b/backend/lang/fr.json index f320c53b..ab0a8706 100644 --- a/backend/lang/fr.json +++ b/backend/lang/fr.json @@ -283,5 +283,53 @@ "Attendee does not belong to this check-in list": "Le participant n'appartient pas à cette liste de pointage", "Attendee :attendee_name\\'s ticket is cancelled": "Le billet de l'participant :attendee_name a été annulé", "Check-in list is not active yet": "La liste d'enregistrement n'est pas encore active", - "The number of attendees does not match the number of tickets in the order": "" -} \ No newline at end of file + "The number of attendees does not match the number of tickets in the order": "Le nombre de participants ne correspond pas au nombre de billets dans la commande.", + "Product is required": "Le produit est requis.", + "Product price is required": "Le prix du produit est requis.", + "Please select at least one product.": "Veuillez sélectionner au moins un produit.", + "The sale start date must be after the product sale start date.": "La date de début de vente doit être après la date de début de vente du produit.", + "You must select a product category.": "Vous devez sélectionner une catégorie de produit.", + "Invalid direction. Must be either asc or desc": "Direction invalide. Doit être soit asc soit desc.", + "DomainObject must be a valid :interface.": "L'objet domaine doit être une :interface valide.", + "Nested relationships must be an array of Relationship objects.": "Les relations imbriquées doivent être un tableau d'objets Relationship.", + "OrderAndDirections must be an array of OrderAndDirection objects.": "OrderAndDirections doit être un tableau d'objets OrderAndDirection.", + "Attendee :attendee_name\\'s product is cancelled": "Le produit du participant :attendee_name est annulé.", + "Tickets": "Billets", + "There are no tickets available for this event.": "Il n'y a pas de billets disponibles pour cet événement.", + "You haven\\'t selected any products": "Vous n'avez sélectionné aucun produit.", + "The maximum number of products available for :products is :max": "Le nombre maximum de produits disponibles pour :products est :max.", + "You must order at least :min products for :product": "Vous devez commander au moins :min produits pour :product.", + "The product :product is sold out": "Le produit :product est épuisé.", + "The maximum number of products available for :product is :max": "Le nombre maximum de produits disponibles pour :product est :max.", + "Sorry, these products are sold out": "Désolé, ces produits sont épuisés.", + "The maximum number of products available is :max": "Le nombre maximum de produits disponibles est :max.", + "Product with id :id not found": "Produit avec id :id introuvable.", + "You cannot delete this product because it has orders associated with it. You can hide it instead.": "Vous ne pouvez pas supprimer ce produit car il a des commandes associées. Vous pouvez le masquer à la place.", + "Invalid product ids: :ids": "Identifiants de produits invalides : :ids.", + "Product is hidden without promo code": "Le produit est masqué sans code promo.", + "Product is sold out": "Le produit est épuisé.", + "Product is before sale start date": "Le produit est avant la date de début de la vente.", + "Product is after sale end date": "Le produit est après la date de fin de la vente.", + "Product is hidden": "Le produit est masqué.", + "Cannot delete product price with id :id because it has sales": "Impossible de supprimer le prix du produit avec id :id car il a des ventes.", + "You cannot delete this product category because it contains the following products: :products. These products are linked to existing orders. Please move the :product_name to another category before attempting to delete this one.": "Vous ne pouvez pas supprimer cette catégorie de produit car elle contient les produits suivants : :products. Ces produits sont liés à des commandes existantes. Veuillez déplacer le :product_name vers une autre catégorie avant d'essayer de supprimer celle-ci.", + "products": "produits", + "product": "produit", + "Product category :productCategoryId has been deleted.": "La catégorie de produit :productCategoryId a été supprimée.", + "You cannot delete the last product category. Please create another category before deleting this one.": "Vous ne pouvez pas supprimer la dernière catégorie de produit. Veuillez en créer une autre avant de supprimer celle-ci.", + "The product category with ID :id was not found.": "La catégorie de produit avec ID :id n'a pas été trouvée.", + "Expired": "Expiré", + "Limit Reached": "Limite atteinte", + "Deleted": "Supprimé", + "Active": "Actif", + "This ticket is invalid": "Ce billet n'est pas valide.", + "There are no tickets available. ' .\n 'If you would like to assign a product to this attendee,' .\n ' please adjust the product\\'s available quantity.": "Il n'y a pas de billets disponibles. Si vous souhaitez attribuer un produit à ce participant, ajustez la quantité disponible du produit.", + "The product price ID is invalid.": "L'ID du prix du produit n'est pas valide.", + "Product ID is not valid": "L'ID du produit n'est pas valide.", + "There are no products available. If you would like to assign this product to this attendee, please adjust the product\\'s available quantity.": "Il n'y a pas de produits disponibles. Si vous souhaitez attribuer ce produit à ce participant, ajustez la quantité disponible du produit.", + "There is an unexpected product price ID in the order": "Il y a un ID de prix de produit inattendu dans la commande.", + "Product type cannot be changed as products have been registered for this type": "Le type de produit ne peut pas être modifié car des produits ont été enregistrés pour ce type.", + "The ordered category IDs must exactly match all categories for the event without missing or extra IDs.": "Les ID de catégorie commandés doivent correspondre exactement à toutes les catégories de l'événement sans ID manquants ou en trop.", + "The ordered product IDs must exactly match all products for the event without missing or extra IDs.": "Les ID de produit commandés doivent correspondre exactement à tous les produits de l'événement sans ID manquants ou en trop.", + "This product is outdated. Please reload the page.": "Ce produit est obsolète. Veuillez recharger la page." +} diff --git a/backend/lang/pt-br.json b/backend/lang/pt-br.json index 5548c202..b1ee9cd6 100644 --- a/backend/lang/pt-br.json +++ b/backend/lang/pt-br.json @@ -283,5 +283,53 @@ "Attendee does not belong to this check-in list": "O participante não pertence a esta lista de check-in", "Attendee :attendee_name\\'s ticket is cancelled": "O ingresso do participante :attendee_name foi cancelado", "Check-in list is not active yet": "A lista de check-in ainda não está ativa", - "The number of attendees does not match the number of tickets in the order": "" -} \ No newline at end of file + "The number of attendees does not match the number of tickets in the order": "O número de participantes não coincide com o número de ingressos no pedido.", + "Product is required": "O produto é obrigatório.", + "Product price is required": "O preço do produto é obrigatório.", + "Please select at least one product.": "Por favor, selecione ao menos um produto.", + "The sale start date must be after the product sale start date.": "A data de início da venda deve ser posterior à data de início da venda do produto.", + "You must select a product category.": "Você deve selecionar uma categoria de produto.", + "Invalid direction. Must be either asc or desc": "Direção inválida. Deve ser asc ou desc.", + "DomainObject must be a valid :interface.": "O objeto de domínio deve ser um :interface válido.", + "Nested relationships must be an array of Relationship objects.": "As relações aninhadas devem ser uma matriz de objetos Relationship.", + "OrderAndDirections must be an array of OrderAndDirection objects.": "OrderAndDirections deve ser uma matriz de objetos OrderAndDirection.", + "Attendee :attendee_name\\'s product is cancelled": "O produto do participante :attendee_name foi cancelado.", + "Tickets": "Ingressos", + "There are no tickets available for this event.": "Não há ingressos disponíveis para este evento.", + "You haven\\'t selected any products": "Você não selecionou nenhum produto.", + "The maximum number of products available for :products is :max": "O número máximo de produtos disponíveis para :products é :max.", + "You must order at least :min products for :product": "Você deve solicitar pelo menos :min produtos para :product.", + "The product :product is sold out": "O produto :product está esgotado.", + "The maximum number of products available for :product is :max": "O número máximo de produtos disponíveis para :product é :max.", + "Sorry, these products are sold out": "Desculpe, esses produtos estão esgotados.", + "The maximum number of products available is :max": "O número máximo de produtos disponíveis é :max.", + "Product with id :id not found": "Produto com id :id não encontrado.", + "You cannot delete this product because it has orders associated with it. You can hide it instead.": "Você não pode excluir este produto porque ele possui pedidos associados. Você pode ocultá-lo.", + "Invalid product ids: :ids": "IDs de produto inválidos: :ids.", + "Product is hidden without promo code": "O produto está oculto sem código promocional.", + "Product is sold out": "O produto está esgotado.", + "Product is before sale start date": "O produto está antes da data de início da venda.", + "Product is after sale end date": "O produto está após a data de término da venda.", + "Product is hidden": "O produto está oculto.", + "Cannot delete product price with id :id because it has sales": "Não é possível excluir o preço do produto com id :id porque ele possui vendas.", + "You cannot delete this product category because it contains the following products: :products. These products are linked to existing orders. Please move the :product_name to another category before attempting to delete this one.": "Você não pode excluir esta categoria de produto porque contém os seguintes produtos: :products. Esses produtos estão vinculados a pedidos existentes. Por favor, mova o :product_name para outra categoria antes de tentar excluir esta.", + "products": "produtos", + "product": "produto", + "Product category :productCategoryId has been deleted.": "A categoria de produto :productCategoryId foi excluída.", + "You cannot delete the last product category. Please create another category before deleting this one.": "Você não pode excluir a última categoria de produto. Por favor, crie uma nova categoria antes de excluir esta.", + "The product category with ID :id was not found.": "A categoria de produto com ID :id não foi encontrada.", + "Expired": "Expirado", + "Limit Reached": "Limite alcançado", + "Deleted": "Deletado", + "Active": "Ativo", + "This ticket is invalid": "Este ingresso não é válido.", + "There are no tickets available. ' .\n 'If you would like to assign a product to this attendee,' .\n ' please adjust the product\\'s available quantity.": "Não há ingressos disponíveis. Caso queira atribuir um produto para este participante, ajuste a quantidade disponível do produto.", + "The product price ID is invalid.": "O ID do preço do produto é inválido.", + "Product ID is not valid": "O ID do produto não é válido.", + "There are no products available. If you would like to assign this product to this attendee, please adjust the product\\'s available quantity.": "Não há produtos disponíveis. Caso queira atribuir este produto a este participante, ajuste a quantidade disponível do produto.", + "There is an unexpected product price ID in the order": "Há um ID de preço de produto inesperado no pedido.", + "Product type cannot be changed as products have been registered for this type": "O tipo de produto não pode ser alterado pois há produtos registrados para este tipo.", + "The ordered category IDs must exactly match all categories for the event without missing or extra IDs.": "Os IDs das categorias solicitadas devem corresponder exatamente a todas as categorias do evento, sem IDs faltando ou extras.", + "The ordered product IDs must exactly match all products for the event without missing or extra IDs.": "Os IDs dos produtos solicitados devem corresponder exatamente a todos os produtos do evento, sem IDs faltando ou extras.", + "This product is outdated. Please reload the page.": "Este produto está desatualizado. Por favor, recarregue a página." +} diff --git a/backend/lang/pt.json b/backend/lang/pt.json index 7ed11c10..5021e225 100644 --- a/backend/lang/pt.json +++ b/backend/lang/pt.json @@ -283,5 +283,53 @@ "Attendee does not belong to this check-in list": "O participante não pertence a esta lista de check-in", "Attendee :attendee_name\\'s ticket is cancelled": "O ingresso do participante :attendee_name foi cancelado", "Check-in list is not active yet": "A lista de check-in ainda não está ativa", - "The number of attendees does not match the number of tickets in the order": "" -} \ No newline at end of file + "The number of attendees does not match the number of tickets in the order": "O número de participantes não corresponde ao número de ingressos no pedido.", + "Product is required": "O produto é obrigatório.", + "Product price is required": "O preço do produto é obrigatório.", + "Please select at least one product.": "Por favor, selecione pelo menos um produto.", + "The sale start date must be after the product sale start date.": "A data de início da venda deve ser após a data de início da venda do produto.", + "You must select a product category.": "Você deve selecionar uma categoria de produto.", + "Invalid direction. Must be either asc or desc": "Direção inválida. Deve ser asc ou desc.", + "DomainObject must be a valid :interface.": "O objeto de domínio deve ser uma :interface válida.", + "Nested relationships must be an array of Relationship objects.": "As relações aninhadas devem ser um array de objetos Relationship.", + "OrderAndDirections must be an array of OrderAndDirection objects.": "OrderAndDirections deve ser um array de objetos OrderAndDirection.", + "Attendee :attendee_name\\'s product is cancelled": "O produto do participante :attendee_name está cancelado.", + "Tickets": "Ingressos", + "There are no tickets available for this event.": "Não há ingressos disponíveis para este evento.", + "You haven\\'t selected any products": "Você não selecionou nenhum produto.", + "The maximum number of products available for :products is :max": "O número máximo de produtos disponíveis para :products é :max.", + "You must order at least :min products for :product": "Você deve pedir pelo menos :min produtos para :product.", + "The product :product is sold out": "O produto :product está esgotado.", + "The maximum number of products available for :product is :max": "O número máximo de produtos disponíveis para :product é :max.", + "Sorry, these products are sold out": "Desculpe, esses produtos estão esgotados.", + "The maximum number of products available is :max": "O número máximo de produtos disponíveis é :max.", + "Product with id :id not found": "Produto com id :id não encontrado.", + "You cannot delete this product because it has orders associated with it. You can hide it instead.": "Você não pode excluir este produto porque ele tem pedidos associados. Você pode ocultá-lo.", + "Invalid product ids: :ids": "IDs de produto inválidos: :ids.", + "Product is hidden without promo code": "O produto está oculto sem código promocional.", + "Product is sold out": "O produto está esgotado.", + "Product is before sale start date": "O produto está antes da data de início da venda.", + "Product is after sale end date": "O produto está após a data de término da venda.", + "Product is hidden": "O produto está oculto.", + "Cannot delete product price with id :id because it has sales": "Não é possível excluir o preço do produto com id :id porque ele tem vendas.", + "You cannot delete this product category because it contains the following products: :products. These products are linked to existing orders. Please move the :product_name to another category before attempting to delete this one.": "Você não pode excluir esta categoria de produto porque ela contém os seguintes produtos: :products. Esses produtos estão vinculados a pedidos existentes. Por favor, mova o :product_name para outra categoria antes de tentar excluir esta.", + "products": "produtos", + "product": "produto", + "Product category :productCategoryId has been deleted.": "A categoria de produto :productCategoryId foi excluída.", + "You cannot delete the last product category. Please create another category before deleting this one.": "Você não pode excluir a última categoria de produto. Por favor, crie outra categoria antes de excluir esta.", + "The product category with ID :id was not found.": "A categoria de produto com ID :id não foi encontrada.", + "Expired": "Expirado", + "Limit Reached": "Limite atingido", + "Deleted": "Excluído", + "Active": "Ativo", + "This ticket is invalid": "Este ingresso não é válido.", + "There are no tickets available. ' .\n 'If you would like to assign a product to this attendee,' .\n ' please adjust the product\\'s available quantity.": "Não há ingressos disponíveis. Se você deseja atribuir um produto a este participante, ajuste a quantidade disponível do produto.", + "The product price ID is invalid.": "O ID do preço do produto não é válido.", + "Product ID is not valid": "O ID do produto não é válido.", + "There are no products available. If you would like to assign this product to this attendee, please adjust the product\\'s available quantity.": "Não há produtos disponíveis. Se você deseja atribuir este produto a este participante, ajuste a quantidade disponível do produto.", + "There is an unexpected product price ID in the order": "Há um ID de preço de produto inesperado no pedido.", + "Product type cannot be changed as products have been registered for this type": "O tipo de produto não pode ser alterado porque os produtos foram registrados para este tipo.", + "The ordered category IDs must exactly match all categories for the event without missing or extra IDs.": "Os IDs de categoria solicitados devem corresponder exatamente a todas as categorias do evento sem IDs faltando ou extras.", + "The ordered product IDs must exactly match all products for the event without missing or extra IDs.": "Os IDs de produtos solicitados devem corresponder exatamente a todos os produtos do evento sem IDs faltando ou extras.", + "This product is outdated. Please reload the page.": "Este produto está desatualizado. Por favor, recarregue a página." +} diff --git a/backend/lang/ru.json b/backend/lang/ru.json index d91adc07..b196d5ff 100644 --- a/backend/lang/ru.json +++ b/backend/lang/ru.json @@ -298,5 +298,53 @@ "Attendee does not belong to this check-in list": "", "Attendee :attendee_name\\'s ticket is cancelled": "", "Check-in list is not active yet": "", - "The number of attendees does not match the number of tickets in the order": "" + "The number of attendees does not match the number of tickets in the order": "", + "Product is required": "", + "Product price is required": "", + "Please select at least one product.": "", + "The sale start date must be after the product sale start date.": "", + "You must select a product category.": "", + "Invalid direction. Must be either asc or desc": "", + "DomainObject must be a valid :interface.": "", + "Nested relationships must be an array of Relationship objects.": "", + "OrderAndDirections must be an array of OrderAndDirection objects.": "", + "Attendee :attendee_name\\'s product is cancelled": "", + "Tickets": "", + "There are no tickets available for this event.": "", + "You haven\\'t selected any products": "", + "The maximum number of products available for :products is :max": "", + "You must order at least :min products for :product": "", + "The product :product is sold out": "", + "The maximum number of products available for :product is :max": "", + "Sorry, these products are sold out": "", + "The maximum number of products available is :max": "", + "Product with id :id not found": "", + "You cannot delete this product because it has orders associated with it. You can hide it instead.": "", + "Invalid product ids: :ids": "", + "Product is hidden without promo code": "", + "Product is sold out": "", + "Product is before sale start date": "", + "Product is after sale end date": "", + "Product is hidden": "", + "Cannot delete product price with id :id because it has sales": "", + "You cannot delete this product category because it contains the following products: :products. These products are linked to existing orders. Please move the :product_name to another category before attempting to delete this one.": "", + "products": "", + "product": "", + "Product category :productCategoryId has been deleted.": "", + "You cannot delete the last product category. Please create another category before deleting this one.": "", + "The product category with ID :id was not found.": "", + "Expired": "", + "Limit Reached": "", + "Deleted": "", + "Active": "", + "This ticket is invalid": "", + "There are no tickets available. ' .\n 'If you would like to assign a product to this attendee,' .\n ' please adjust the product\\'s available quantity.": "", + "The product price ID is invalid.": "", + "Product ID is not valid": "", + "There are no products available. If you would like to assign this product to this attendee, please adjust the product\\'s available quantity.": "", + "There is an unexpected product price ID in the order": "", + "Product type cannot be changed as products have been registered for this type": "", + "The ordered category IDs must exactly match all categories for the event without missing or extra IDs.": "", + "The ordered product IDs must exactly match all products for the event without missing or extra IDs.": "", + "This product is outdated. Please reload the page.": "" } \ No newline at end of file diff --git a/backend/lang/zh-cn.json b/backend/lang/zh-cn.json index ac84d37b..833b13fc 100644 --- a/backend/lang/zh-cn.json +++ b/backend/lang/zh-cn.json @@ -283,5 +283,53 @@ "Attendee does not belong to this check-in list": "该参与者不属于此签到列表", "Attendee :attendee_name\\'s ticket is cancelled": "参与者 :attendee_name 的票已被取消", "Check-in list is not active yet": "签到列表尚未激活", - "The number of attendees does not match the number of tickets in the order": "" -} \ No newline at end of file + "The number of attendees does not match the number of tickets in the order": "参加者的数量与订单中的票数不匹配。", + "Product is required": "需要产品。", + "Product price is required": "需要产品价格。", + "Please select at least one product.": "请选择至少一个产品。", + "The sale start date must be after the product sale start date.": "销售开始日期必须晚于产品销售开始日期。", + "You must select a product category.": "您必须选择一个产品类别。", + "Invalid direction. Must be either asc or desc": "方向无效。必须是升序 (asc) 或降序 (desc)。", + "DomainObject must be a valid :interface.": "DomainObject 必须是一个有效的 :interface。", + "Nested relationships must be an array of Relationship objects.": "嵌套关系必须是 Relationship 对象的数组。", + "OrderAndDirections must be an array of OrderAndDirection objects.": "OrderAndDirections 必须是 OrderAndDirection 对象的数组。", + "Attendee :attendee_name\\'s product is cancelled": "参加者 :attendee_name 的产品已取消。", + "Tickets": "票", + "There are no tickets available for this event.": "此活动没有可用的票。", + "You haven\\'t selected any products": "您尚未选择任何产品。", + "The maximum number of products available for :products is :max": ":products 的最大可用数量是 :max。", + "You must order at least :min products for :product": "您必须为 :product 至少订购 :min 件产品。", + "The product :product is sold out": "产品 :product 已售罄。", + "The maximum number of products available for :product is :max": "产品 :product 的最大可用数量是 :max。", + "Sorry, these products are sold out": "抱歉,这些产品已售罄。", + "The maximum number of products available is :max": "最大可用产品数量为 :max。", + "Product with id :id not found": "未找到 ID 为 :id 的产品。", + "You cannot delete this product because it has orders associated with it. You can hide it instead.": "您无法删除此产品,因为它已与订单关联。您可以将其隐藏。", + "Invalid product ids: :ids": "产品 ID 无效::ids。", + "Product is hidden without promo code": "没有促销代码时产品隐藏。", + "Product is sold out": "产品已售罄。", + "Product is before sale start date": "产品尚未到销售开始日期。", + "Product is after sale end date": "产品已超过销售结束日期。", + "Product is hidden": "产品已隐藏。", + "Cannot delete product price with id :id because it has sales": "无法删除 ID 为 :id 的产品价格,因为它已有销售记录。", + "You cannot delete this product category because it contains the following products: :products. These products are linked to existing orders. Please move the :product_name to another category before attempting to delete this one.": "您无法删除此产品类别,因为它包含以下产品::products。这些产品已关联到现有订单。请将 :product_name 移到另一个类别后再尝试删除。", + "products": "产品", + "product": "产品", + "Product category :productCategoryId has been deleted.": "产品类别 :productCategoryId 已被删除。", + "You cannot delete the last product category. Please create another category before deleting this one.": "您无法删除最后一个产品类别。请先创建另一个类别。", + "The product category with ID :id was not found.": "未找到 ID 为 :id 的产品类别。", + "Expired": "已过期", + "Limit Reached": "达到限制", + "Deleted": "已删除", + "Active": "活动中", + "This ticket is invalid": "此票无效。", + "There are no tickets available. ' .\n 'If you would like to assign a product to this attendee,' .\n ' please adjust the product\\'s available quantity.": "没有可用票。如果您想为此参加者分配产品,请调整产品的可用数量。", + "The product price ID is invalid.": "产品价格 ID 无效。", + "Product ID is not valid": "产品 ID 无效。", + "There are no products available. If you would like to assign this product to this attendee, please adjust the product\\'s available quantity.": "没有可用的产品。如果您想为此参加者分配此产品,请调整产品的可用数量。", + "There is an unexpected product price ID in the order": "订单中存在意外的产品价格 ID。", + "Product type cannot be changed as products have been registered for this type": "无法更改产品类型,因为此类型的产品已注册。", + "The ordered category IDs must exactly match all categories for the event without missing or extra IDs.": "所订购的类别 ID 必须与活动的所有类别完全匹配,不能有遗漏或多余的 ID。", + "The ordered product IDs must exactly match all products for the event without missing or extra IDs.": "所订购的产品 ID 必须与活动的所有产品完全匹配,不能有遗漏或多余的 ID。", + "This product is outdated. Please reload the page.": "此产品已过时。请重新加载页面。" +} diff --git a/backend/resources/views/emails/event/message.blade.php b/backend/resources/views/emails/event/message.blade.php index 867fa948..65386cc5 100644 --- a/backend/resources/views/emails/event/message.blade.php +++ b/backend/resources/views/emails/event/message.blade.php @@ -1,6 +1,6 @@ @php /** @var \HiEvents\DomainObjects\EventDomainObject $event */ @endphp @php /** @var \HiEvents\DomainObjects\EventSettingDomainObject $eventSettings */ @endphp -@php /** @var \HiEvents\Services\Handlers\Message\DTO\SendMessageDTO $messageData */ @endphp +@php /** @var \HiEvents\Services\Application\Handlers\Message\DTO\SendMessageDTO $messageData */ @endphp @php /** @see \HiEvents\Mail\Event\EventMessage */ @endphp diff --git a/backend/routes/api.php b/backend/routes/api.php index 540639f1..e04ac8b4 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -14,11 +14,14 @@ use HiEvents\Http\Actions\Attendees\PartialEditAttendeeAction; use HiEvents\Http\Actions\Attendees\ResendAttendeeTicketAction; use HiEvents\Http\Actions\Auth\AcceptInvitationAction; +use HiEvents\Http\Actions\Auth\CreateApiKeyAction; use HiEvents\Http\Actions\Auth\ForgotPasswordAction; +use HiEvents\Http\Actions\Auth\GetApiKeysAction; use HiEvents\Http\Actions\Auth\GetUserInvitationAction; use HiEvents\Http\Actions\Auth\LoginAction; use HiEvents\Http\Actions\Auth\LogoutAction; use HiEvents\Http\Actions\Auth\RefreshTokenAction; +use HiEvents\Http\Actions\Auth\RevokeApiKeyAction; use HiEvents\Http\Actions\Auth\ResetPasswordAction; use HiEvents\Http\Actions\Auth\ValidateResetPasswordTokenAction; use HiEvents\Http\Actions\CapacityAssignments\CreateCapacityAssignmentAction; @@ -70,6 +73,11 @@ use HiEvents\Http\Actions\Organizers\GetOrganizerAction; use HiEvents\Http\Actions\Organizers\GetOrganizerEventsAction; use HiEvents\Http\Actions\Organizers\GetOrganizersAction; +use HiEvents\Http\Actions\ProductCategories\CreateProductCategoryAction; +use HiEvents\Http\Actions\ProductCategories\DeleteProductCategoryAction; +use HiEvents\Http\Actions\ProductCategories\EditProductCategoryAction; +use HiEvents\Http\Actions\ProductCategories\GetProductCategoriesAction; +use HiEvents\Http\Actions\ProductCategories\GetProductCategoryAction; use HiEvents\Http\Actions\PromoCodes\CreatePromoCodeAction; use HiEvents\Http\Actions\PromoCodes\DeletePromoCodeAction; use HiEvents\Http\Actions\PromoCodes\GetPromoCodeAction; @@ -83,16 +91,17 @@ use HiEvents\Http\Actions\Questions\GetQuestionsAction; use HiEvents\Http\Actions\Questions\GetQuestionsPublicAction; use HiEvents\Http\Actions\Questions\SortQuestionsAction; +use HiEvents\Http\Actions\Reports\GetReportAction; use HiEvents\Http\Actions\TaxesAndFees\CreateTaxOrFeeAction; use HiEvents\Http\Actions\TaxesAndFees\DeleteTaxOrFeeAction; use HiEvents\Http\Actions\TaxesAndFees\EditTaxOrFeeAction; use HiEvents\Http\Actions\TaxesAndFees\GetTaxOrFeeAction; -use HiEvents\Http\Actions\Tickets\CreateTicketAction; -use HiEvents\Http\Actions\Tickets\DeleteTicketAction; -use HiEvents\Http\Actions\Tickets\EditTicketAction; -use HiEvents\Http\Actions\Tickets\GetTicketAction; -use HiEvents\Http\Actions\Tickets\GetTicketsAction; -use HiEvents\Http\Actions\Tickets\SortTicketsAction; +use HiEvents\Http\Actions\Products\CreateProductAction; +use HiEvents\Http\Actions\Products\DeleteProductAction; +use HiEvents\Http\Actions\Products\EditProductAction; +use HiEvents\Http\Actions\Products\GetProductAction; +use HiEvents\Http\Actions\Products\GetProductsAction; +use HiEvents\Http\Actions\Products\SortProductsAction; use HiEvents\Http\Actions\Users\CancelEmailChangeAction; use HiEvents\Http\Actions\Users\ConfirmEmailAddressAction; use HiEvents\Http\Actions\Users\ConfirmEmailChangeAction; @@ -127,112 +136,202 @@ function (Router $router): void { ); /** - * Logged In Routes + * Routes only for authenticated users (not API keys) */ $router->middleware(['auth:api'])->group( function (Router $router): void { $router->get('/auth/logout', LogoutAction::class); $router->post('/auth/refresh', RefreshTokenAction::class); + $router->get('/api-keys', GetApiKeysAction::class); + $router->post('/api-keys', CreateApiKeyAction::class); + $router->delete('/api-keys/{api_key}', RevokeApiKeyAction::class); + } +); + +/** + * Logged In Routes + */ +$router->middleware(['auth:sanctum'])->group( + function (Router $router): void { + $router->get('/users/me', GetMeAction::class); - $router->put('/users/me', UpdateMeAction::class); - $router->post('/users', CreateUserAction::class); - $router->get('/users', GetUsersAction::class); - $router->get('/users/{user_id}', GetUserAction::class); - $router->put('/users/{user_id}', UpdateUserAction::class); - $router->delete('/users/{user_id}', DeactivateUsersAction::class); - $router->post('/users/{user_id}/email-change/{token}', ConfirmEmailChangeAction::class); - $router->post('/users/{user_id}/invitation', ResendInvitationAction::class); - $router->delete('/users/{user_id}/invitation', DeleteInvitationAction::class); - $router->delete('/users/{user_id}/email-change', CancelEmailChangeAction::class); - $router->post('/users/{user_id}/confirm-email/{token}', ConfirmEmailAddressAction::class); - $router->post('/users/{user_id}/resend-email-confirmation', ResendEmailConfirmationAction::class); - - $router->get('/accounts/{account_id?}', GetAccountAction::class); - $router->put('/accounts/{account_id?}', UpdateAccountAction::class); - $router->post('/accounts/{account_id}/stripe/connect', CreateStripeConnectAccountAction::class); - - $router->post('/organizers', CreateOrganizerAction::class); - // This is POST instead of PUT because you can't upload files via PUT in PHP (at least not easily) - $router->post('/organizers/{organizer_id}', EditOrganizerAction::class); - $router->get('/organizers', GetOrganizersAction::class); - $router->get('/organizers/{organizer_id}', GetOrganizerAction::class); - $router->get('/organizers/{organizer_id}/events', GetOrganizerEventsAction::class); - - $router->post('/accounts/{account_id}/taxes-and-fees', CreateTaxOrFeeAction::class); - $router->get('/accounts/{account_id}/taxes-and-fees', GetTaxOrFeeAction::class); - $router->put('/accounts/{account_id}/taxes-and-fees/{tax_or_fee_id}', EditTaxOrFeeAction::class); - $router->delete('/accounts/{account_id}/taxes-and-fees/{tax_or_fee_id}', DeleteTaxOrFeeAction::class); - - $router->post('/events', CreateEventAction::class); - $router->get('/events', GetEventsAction::class); - $router->get('/events/{event_id}', GetEventAction::class); - $router->put('/events/{event_id}', UpdateEventAction::class); - $router->put('/events/{event_id}/status', UpdateEventStatusAction::class); - $router->post('/events/{event_id}/duplicate', DuplicateEventAction::class); - - $router->post('/events/{event_id}/tickets', CreateTicketAction::class); - $router->post('/events/{event_id}/tickets/sort', SortTicketsAction::class); - $router->put('/events/{event_id}/tickets/{ticket_id}', EditTicketAction::class); - $router->get('/events/{event_id}/tickets/{ticket_id}', GetTicketAction::class); - $router->delete('/events/{event_id}/tickets/{ticket_id}', DeleteTicketAction::class); - $router->get('/events/{event_id}/tickets', GetTicketsAction::class); - $router->get('/events/{event_id}/check_in_stats', GetEventCheckInStatsAction::class); - $router->get('/events/{event_id}/stats', GetEventStatsAction::class); - - $router->post('/events/{event_id}/attendees', CreateAttendeeAction::class); - $router->get('/events/{event_id}/attendees', GetAttendeesAction::class); - $router->get('/events/{event_id}/attendees/{attendee_id}', GetAttendeeAction::class); - $router->put('/events/{event_id}/attendees/{attendee_id}', EditAttendeeAction::class); - $router->patch('/events/{event_id}/attendees/{attendee_id}', PartialEditAttendeeAction::class); - $router->post('/events/{event_id}/attendees/export', ExportAttendeesAction::class); - $router->post('/events/{event_id}/attendees/{attendee_public_id}/resend-ticket', ResendAttendeeTicketAction::class); - $router->post('/events/{event_id}/attendees/{attendee_public_id}/check_in', CheckInAttendeeAction::class); - - $router->get('/events/{event_id}/orders', GetOrdersAction::class); - $router->get('/events/{event_id}/orders/{order_id}', GetOrderAction::class); - $router->post('/events/{event_id}/orders/{order_id}/message', MessageOrderAction::class); - $router->post('/events/{event_id}/orders/{order_id}/refund', RefundOrderAction::class); - $router->post('/events/{event_id}/orders/{order_id}/resend_confirmation', ResendOrderConfirmationAction::class); - $router->post('/events/{event_id}/orders/{order_id}/cancel', CancelOrderAction::class); - $router->post('/events/{event_id}/orders/export', ExportOrdersAction::class); - - $router->post('/events/{event_id}/questions', CreateQuestionAction::class); - $router->put('/events/{event_id}/questions/{question_id}', EditQuestionAction::class); - $router->get('/events/{event_id}/questions/{question_id}', GetQuestionAction::class); - $router->delete('/events/{event_id}/questions/{question_id}', DeleteQuestionAction::class); - $router->get('/events/{event_id}/questions', GetQuestionsAction::class); - $router->post('/events/{event_id}/questions/export', ExportOrdersAction::class); - $router->post('/events/{event_id}/questions/sort', SortQuestionsAction::class); - - $router->post('/events/{event_id}/images', CreateEventImageAction::class); - $router->get('/events/{event_id}/images', GetEventImagesAction::class); - $router->delete('/events/{event_id}/images/{image_id}', DeleteEventImageAction::class); - - $router->post('/events/{event_id}/promo-codes', CreatePromoCodeAction::class); - $router->put('/events/{event_id}/promo-codes/{promo_code_id}', UpdatePromoCodeAction::class); - $router->get('/events/{event_id}/promo-codes', GetPromoCodesAction::class); - $router->get('/events/{event_id}/promo-codes/{promo_code_id}', GetPromoCodeAction::class); - $router->delete('/events/{event_id}/promo-codes/{promo_code_id}', DeletePromoCodeAction::class); - - $router->post('/events/{event_id}/messages', SendMessageAction::class); - $router->get('/events/{event_id}/messages', GetMessagesAction::class); - - $router->get('/events/{event_id}/settings', GetEventSettingsAction::class); - $router->put('/events/{event_id}/settings', EditEventSettingsAction::class); - $router->patch('/events/{event_id}/settings', PartialEditEventSettingsAction::class); - - $router->post('/events/{event_id}/capacity-assignments', CreateCapacityAssignmentAction::class); - $router->get('/events/{event_id}/capacity-assignments', GetCapacityAssignmentsAction::class); - $router->get('/events/{event_id}/capacity-assignments/{capacity_assignment_id}', GetCapacityAssignmentAction::class); - $router->put('/events/{event_id}/capacity-assignments/{capacity_assignment_id}', UpdateCapacityAssignmentAction::class); - $router->delete('/events/{event_id}/capacity-assignments/{capacity_assignment_id}', DeleteCapacityAssignmentAction::class); - - $router->post('/events/{event_id}/check-in-lists', CreateCheckInListAction::class); - $router->get('/events/{event_id}/check-in-lists', GetCheckInListsAction::class); - $router->get('/events/{event_id}/check-in-lists/{check_in_list_id}', GetCheckInListAction::class); - $router->put('/events/{event_id}/check-in-lists/{check_in_list_id}', UpdateCheckInListAction::class); - $router->delete('/events/{event_id}/check-in-lists/{check_in_list_id}', DeleteCheckInListAction::class); + + $router->middleware(['ability:users'])->group( + function (Router $router): void { + $router->put('/users/me', UpdateMeAction::class); + $router->post('/users', CreateUserAction::class); + $router->get('/users', GetUsersAction::class); + $router->get('/users/{user_id}', GetUserAction::class); + $router->put('/users/{user_id}', UpdateUserAction::class); + $router->delete('/users/{user_id}', DeactivateUsersAction::class); + $router->post('/users/{user_id}/email-change/{token}', ConfirmEmailChangeAction::class); + $router->post('/users/{user_id}/invitation', ResendInvitationAction::class); + $router->delete('/users/{user_id}/invitation', DeleteInvitationAction::class); + $router->delete('/users/{user_id}/email-change', CancelEmailChangeAction::class); + $router->post('/users/{user_id}/confirm-email/{token}', ConfirmEmailAddressAction::class); + $router->post('/users/{user_id}/resend-email-confirmation', ResendEmailConfirmationAction::class); + } + ); + + $router->middleware(['ability:accounts'])->group( + function (Router $router): void { + $router->get('/accounts/{account_id?}', GetAccountAction::class); + $router->put('/accounts/{account_id?}', UpdateAccountAction::class); + $router->post('/accounts/{account_id}/stripe/connect', CreateStripeConnectAccountAction::class); + } + ); + + $router->middleware(['ability:organizers'])->group( + function (Router $router): void { + $router->post('/organizers', CreateOrganizerAction::class); + // This is POST instead of PUT because you can't upload files via PUT in PHP (at least not easily) + $router->post('/organizers/{organizer_id}', EditOrganizerAction::class); + $router->get('/organizers', GetOrganizersAction::class); + $router->get('/organizers/{organizer_id}', GetOrganizerAction::class); + $router->get('/organizers/{organizer_id}/events', GetOrganizerEventsAction::class); + } + ); + + $router->middleware(['ability:taxes-and-fees'])->group( + function (Router $router): void { + $router->post('/accounts/{account_id}/taxes-and-fees', CreateTaxOrFeeAction::class); + $router->get('/accounts/{account_id}/taxes-and-fees', GetTaxOrFeeAction::class); + $router->put('/accounts/{account_id}/taxes-and-fees/{tax_or_fee_id}', EditTaxOrFeeAction::class); + $router->delete('/accounts/{account_id}/taxes-and-fees/{tax_or_fee_id}', DeleteTaxOrFeeAction::class); + } + ); + + $router->middleware(['ability:events,events-general'])->group( + function (Router $router): void { + $router->post('/events', CreateEventAction::class); + $router->get('/events', GetEventsAction::class); + $router->get('/events/{event_id}', GetEventAction::class); + $router->put('/events/{event_id}', UpdateEventAction::class); + $router->put('/events/{event_id}/status', UpdateEventStatusAction::class); + $router->post('/events/{event_id}/duplicate', DuplicateEventAction::class); + } + ); + + $router->middleware(['ability:events,events-products'])->group( + function (Router $router): void { + $router->post('/events/{event_id}/product-categories', CreateProductCategoryAction::class); + $router->get('/events/{event_id}/product-categories', GetProductCategoriesAction::class); + $router->get('/events/{event_id}/product-categories/{category_id}', GetProductCategoryAction::class); + $router->put('/events/{event_id}/product-categories/{category_id}', EditProductCategoryAction::class); + $router->delete('/events/{event_id}/product-categories/{category_id}', DeleteProductCategoryAction::class); + + $router->post('/events/{event_id}/products', CreateProductAction::class); + $router->post('/events/{event_id}/products/sort', SortProductsAction::class); + $router->put('/events/{event_id}/products/{ticket_id}', EditProductAction::class); + $router->get('/events/{event_id}/products/{ticket_id}', GetProductAction::class); + $router->delete('/events/{event_id}/products/{ticket_id}', DeleteProductAction::class); + $router->get('/events/{event_id}/products', GetProductsAction::class); + } + ); + + $router->middleware(['ability:events,events-stats'])->group( + function (Router $router): void { + $router->get('/events/{event_id}/check_in_stats', GetEventCheckInStatsAction::class); + $router->get('/events/{event_id}/stats', GetEventStatsAction::class); + } + ); + + $router->middleware(['ability:events,events-attendees'])->group( + function (Router $router): void { + $router->post('/events/{event_id}/attendees', CreateAttendeeAction::class); + $router->get('/events/{event_id}/attendees', GetAttendeesAction::class); + $router->get('/events/{event_id}/attendees/{attendee_id}', GetAttendeeAction::class); + $router->put('/events/{event_id}/attendees/{attendee_id}', EditAttendeeAction::class); + $router->patch('/events/{event_id}/attendees/{attendee_id}', PartialEditAttendeeAction::class); + $router->post('/events/{event_id}/attendees/export', ExportAttendeesAction::class); + $router->post('/events/{event_id}/attendees/{attendee_public_id}/resend-ticket', ResendAttendeeTicketAction::class); + $router->post('/events/{event_id}/attendees/{attendee_public_id}/check_in', CheckInAttendeeAction::class); + } + ); + + $router->middleware(['ability:events,events-orders'])->group( + function (Router $router): void { + $router->get('/events/{event_id}/orders', GetOrdersAction::class); + $router->get('/events/{event_id}/orders/{order_id}', GetOrderAction::class); + $router->post('/events/{event_id}/orders/{order_id}/message', MessageOrderAction::class); + $router->post('/events/{event_id}/orders/{order_id}/refund', RefundOrderAction::class); + $router->post('/events/{event_id}/orders/{order_id}/resend_confirmation', ResendOrderConfirmationAction::class); + $router->post('/events/{event_id}/orders/{order_id}/cancel', CancelOrderAction::class); + $router->post('/events/{event_id}/orders/export', ExportOrdersAction::class); + } + ); + + $router->middleware(['ability:events,events-questions'])->group( + function (Router $router): void { + $router->post('/events/{event_id}/questions', CreateQuestionAction::class); + $router->put('/events/{event_id}/questions/{question_id}', EditQuestionAction::class); + $router->get('/events/{event_id}/questions/{question_id}', GetQuestionAction::class); + $router->delete('/events/{event_id}/questions/{question_id}', DeleteQuestionAction::class); + $router->get('/events/{event_id}/questions', GetQuestionsAction::class); + $router->post('/events/{event_id}/questions/export', ExportOrdersAction::class); + $router->post('/events/{event_id}/questions/sort', SortQuestionsAction::class); + } + ); + + $router->middleware(['ability:events,events-images'])->group( + function (Router $router): void { + $router->post('/events/{event_id}/images', CreateEventImageAction::class); + $router->get('/events/{event_id}/images', GetEventImagesAction::class); + $router->delete('/events/{event_id}/images/{image_id}', DeleteEventImageAction::class); + } + ); + + $router->middleware(['ability:events,events-promo-codes'])->group( + function (Router $router): void { + $router->post('/events/{event_id}/promo-codes', CreatePromoCodeAction::class); + $router->put('/events/{event_id}/promo-codes/{promo_code_id}', UpdatePromoCodeAction::class); + $router->get('/events/{event_id}/promo-codes', GetPromoCodesAction::class); + $router->get('/events/{event_id}/promo-codes/{promo_code_id}', GetPromoCodeAction::class); + $router->delete('/events/{event_id}/promo-codes/{promo_code_id}', DeletePromoCodeAction::class); + } + ); + + $router->middleware(['ability:events,events-messages'])->group( + function (Router $router): void { + $router->post('/events/{event_id}/messages', SendMessageAction::class); + $router->get('/events/{event_id}/messages', GetMessagesAction::class); + } + ); + + $router->middleware(['ability:events,events-settings'])->group( + function (Router $router): void { + $router->get('/events/{event_id}/settings', GetEventSettingsAction::class); + $router->put('/events/{event_id}/settings', EditEventSettingsAction::class); + $router->patch('/events/{event_id}/settings', PartialEditEventSettingsAction::class); + } + ); + + $router->middleware(['ability:events,events-capacity-assignments'])->group( + function (Router $router): void { + $router->post('/events/{event_id}/capacity-assignments', CreateCapacityAssignmentAction::class); + $router->get('/events/{event_id}/capacity-assignments', GetCapacityAssignmentsAction::class); + $router->get('/events/{event_id}/capacity-assignments/{capacity_assignment_id}', GetCapacityAssignmentAction::class); + $router->put('/events/{event_id}/capacity-assignments/{capacity_assignment_id}', UpdateCapacityAssignmentAction::class); + $router->delete('/events/{event_id}/capacity-assignments/{capacity_assignment_id}', DeleteCapacityAssignmentAction::class); + } + ); + + $router->middleware(['ability:events,events-check-in-lists'])->group( + function (Router $router): void { + $router->post('/events/{event_id}/check-in-lists', CreateCheckInListAction::class); + $router->get('/events/{event_id}/check-in-lists', GetCheckInListsAction::class); + $router->get('/events/{event_id}/check-in-lists/{check_in_list_id}', GetCheckInListAction::class); + $router->put('/events/{event_id}/check-in-lists/{check_in_list_id}', UpdateCheckInListAction::class); + $router->delete('/events/{event_id}/check-in-lists/{check_in_list_id}', DeleteCheckInListAction::class); + } + ); + + $router->middleware(['ability:events,events-reports'])->group( + function (Router $router): void { + $router->get('/events/{event_id}/reports/{report_type}', GetReportAction::class); + } + ); } ); @@ -244,8 +343,8 @@ function (Router $router): void { // Events $router->get('/events/{event_id}', GetEventPublicAction::class); - // Tickets - $router->get('/events/{event_id}/tickets', GetEventPublicAction::class); + // Products + $router->get('/events/{event_id}/products', GetEventPublicAction::class); // Orders $router->post('/events/{event_id}/order', CreateOrderActionPublic::class); @@ -276,4 +375,6 @@ function (Router $router): void { } ); -include_once __DIR__ . '/mail.php'; +$router->get('/csrf-cookie', 'Laravel\Sanctum\Http\Controllers\CsrfCookieController@show'); + +include_once __DIR__ . '/mail.php'; \ No newline at end of file diff --git a/backend/tests/Unit/Services/Handlers/Event/GetPublicEventHandlerTest.php b/backend/tests/Unit/Services/Application/Handlers/Event/GetPublicEventHandlerTest.php similarity index 74% rename from backend/tests/Unit/Services/Handlers/Event/GetPublicEventHandlerTest.php rename to backend/tests/Unit/Services/Application/Handlers/Event/GetPublicEventHandlerTest.php index a96224f2..9dffed46 100644 --- a/backend/tests/Unit/Services/Handlers/Event/GetPublicEventHandlerTest.php +++ b/backend/tests/Unit/Services/Application/Handlers/Event/GetPublicEventHandlerTest.php @@ -1,15 +1,15 @@ eventRepository = m::mock(EventRepositoryInterface::class); $this->promoCodeRepository = m::mock(PromoCodeRepositoryInterface::class); - $this->ticketFilterService = m::mock(TicketFilterService::class); + $this->ticketFilterService = m::mock(ProductFilterService::class); $this->eventPageViewIncrementService = m::mock(EventPageViewIncrementService::class); $this->handler = new GetPublicEventHandler( @@ -41,14 +41,12 @@ protected function setUp(): void public function testHandleWithoutPromoCodeAndUnauthenticatedUser(): void { $data = new GetPublicEventDTO(eventId: 1, isAuthenticated: false, ipAddress: '127.0.0.1', promoCode: null); - $tickets = collect(); - $event = m::mock(EventDomainObject::class); - $event->shouldReceive('setTickets')->once()->andReturnSelf(); - $event->shouldReceive('getTickets')->once()->andReturn($tickets); + $event = new EventDomainObject(); + $event->setProductCategories(collect()); $this->setupEventRepositoryMock($event, $data->eventId); $this->promoCodeRepository->shouldReceive('findFirstWhere')->once()->andReturnNull(); - $this->ticketFilterService->shouldReceive('filter')->once()->with($tickets, null)->andReturn(collect()); + $this->ticketFilterService->shouldReceive('filter')->once()->withAnyArgs()->andReturn(collect()); $this->eventPageViewIncrementService->shouldReceive('increment')->once()->with($data->eventId, $data->ipAddress); $this->handler->handle($data); @@ -57,16 +55,14 @@ public function testHandleWithoutPromoCodeAndUnauthenticatedUser(): void public function testHandleWithInvalidPromoCode(): void { $data = new GetPublicEventDTO(eventId: 1, isAuthenticated: false, ipAddress: '127.0.0.1', promoCode: 'INVALID'); - $event = m::mock(EventDomainObject::class); - $tickets = collect(); - $event->shouldReceive('setTickets')->once()->andReturnSelf(); - $event->shouldReceive('getTickets')->once()->andReturn($tickets); + $event = new EventDomainObject(); + $event->setProductCategories(collect()); $promoCode = m::mock(PromoCodeDomainObject::class)->makePartial(); $promoCode->shouldReceive('isValid')->andReturn(false); $this->setupEventRepositoryMock($event, $data->eventId); $this->promoCodeRepository->shouldReceive('findFirstWhere')->once()->andReturn($promoCode); - $this->ticketFilterService->shouldReceive('filter')->once()->with($tickets, null)->andReturn(collect()); + $this->ticketFilterService->shouldReceive('filter')->once()->withAnyArgs()->andReturn(collect()); $this->eventPageViewIncrementService->shouldReceive('increment')->once()->with($data->eventId, $data->ipAddress); $this->handler->handle($data); @@ -75,16 +71,14 @@ public function testHandleWithInvalidPromoCode(): void public function testHandleWithValidPromoCode(): void { $data = new GetPublicEventDTO(eventId: 1, isAuthenticated: false, ipAddress: '127.0.0.1', promoCode: 'VALID'); - $tickets = collect(); - $event = m::mock(EventDomainObject::class); - $event->shouldReceive('setTickets')->once()->andReturnSelf(); - $event->shouldReceive('getTickets')->once()->andReturn($tickets); + $event = new EventDomainObject(); + $event->setProductCategories(collect()); $promoCode = m::mock(PromoCodeDomainObject::class)->makePartial(); $promoCode->shouldReceive('isValid')->andReturn(true); $this->setupEventRepositoryMock($event, $data->eventId); $this->promoCodeRepository->shouldReceive('findFirstWhere')->once()->andReturn($promoCode); - $this->ticketFilterService->shouldReceive('filter')->once()->with($tickets, $promoCode)->andReturn(collect()); + $this->ticketFilterService->shouldReceive('filter')->once()->withAnyArgs()->andReturn(collect()); $this->eventPageViewIncrementService->shouldReceive('increment')->once()->with($data->eventId, $data->ipAddress); $this->handler->handle($data); diff --git a/backend/tests/Unit/Services/Handlers/Order/CompleteOrderHandlerTest.php b/backend/tests/Unit/Services/Application/Handlers/Order/CompleteOrderHandlerTest.php similarity index 75% rename from backend/tests/Unit/Services/Handlers/Order/CompleteOrderHandlerTest.php rename to backend/tests/Unit/Services/Application/Handlers/Order/CompleteOrderHandlerTest.php index a68f595e..5fb1f825 100644 --- a/backend/tests/Unit/Services/Handlers/Order/CompleteOrderHandlerTest.php +++ b/backend/tests/Unit/Services/Application/Handlers/Order/CompleteOrderHandlerTest.php @@ -1,24 +1,25 @@ orderRepository = Mockery::mock(OrderRepositoryInterface::class); $this->attendeeRepository = Mockery::mock(AttendeeRepositoryInterface::class); $this->questionAnswersRepository = Mockery::mock(QuestionAnswerRepositoryInterface::class); - $this->ticketQuantityUpdateService = Mockery::mock(TicketQuantityUpdateService::class); - $this->ticketPriceRepository = Mockery::mock(TicketPriceRepositoryInterface::class); + $this->productQuantityUpdateService = Mockery::mock(ProductQuantityUpdateService::class); + $this->productPriceRepository = Mockery::mock(ProductPriceRepositoryInterface::class); $this->completeOrderHandler = new CompleteOrderHandler( $this->orderRepository, $this->attendeeRepository, $this->questionAnswersRepository, - $this->ticketQuantityUpdateService, - $this->ticketPriceRepository + $this->productQuantityUpdateService, + $this->productPriceRepository ); } @@ -80,12 +80,12 @@ public function testHandleSuccessfullyCompletesOrder(): void $this->orderRepository->shouldReceive('updateFromArray')->andReturn($updatedOrder); $this->orderRepository->shouldReceive('loadRelation')->andReturnSelf(); - $this->ticketPriceRepository->shouldReceive('findWhereIn')->andReturn(new Collection([$this->createMockTicketPrice()])); + $this->productPriceRepository->shouldReceive('findWhereIn')->andReturn(new Collection([$this->createMockProductPrice()])); $this->attendeeRepository->shouldReceive('insert')->andReturn(true); - $this->attendeeRepository->shouldReceive('findWhere')->andReturn(new Collection([$this->createMockAttendee()])); + $this->attendeeRepository->shouldReceive('findWhereIn')->andReturn(new Collection([$this->createMockAttendee()])); - $this->ticketQuantityUpdateService->shouldReceive('updateQuantitiesFromOrder'); + $this->productQuantityUpdateService->shouldReceive('updateQuantitiesFromOrder'); $this->completeOrderHandler->handle($orderShortId, $orderData); @@ -140,7 +140,7 @@ public function testHandleThrowsResourceConflictExceptionWhenOrderExpired(): voi $this->completeOrderHandler->handle($orderShortId, $orderData); } - public function testHandleUpdatesTicketQuantitiesForFreeOrder(): void + public function testHandleUpdatesProductQuantitiesForFreeOrder(): void { $orderShortId = 'ABC123'; $orderData = $this->createMockCompleteOrderDTO(); @@ -152,19 +152,19 @@ public function testHandleUpdatesTicketQuantitiesForFreeOrder(): void $this->orderRepository->shouldReceive('loadRelation')->andReturnSelf(); $this->orderRepository->shouldReceive('updateFromArray')->andReturn($updatedOrder); - $this->ticketPriceRepository->shouldReceive('findWhereIn')->andReturn(new Collection([$this->createMockTicketPrice()])); + $this->productPriceRepository->shouldReceive('findWhereIn')->andReturn(new Collection([$this->createMockProductPrice()])); $this->attendeeRepository->shouldReceive('insert')->andReturn(true); - $this->attendeeRepository->shouldReceive('findWhere')->andReturn(new Collection([$this->createMockAttendee()])); + $this->attendeeRepository->shouldReceive('findWhereIn')->andReturn(new Collection([$this->createMockAttendee()])); - $this->ticketQuantityUpdateService->shouldReceive('updateQuantitiesFromOrder')->once(); + $this->productQuantityUpdateService->shouldReceive('updateQuantitiesFromOrder')->once(); $order = $this->completeOrderHandler->handle($orderShortId, $orderData); $this->assertSame($order->getStatus(), OrderStatus::COMPLETED->name); } - public function testHandleDoesNotUpdateTicketQuantitiesForPaidOrder(): void + public function testHandleDoesNotUpdateProductQuantitiesForPaidOrder(): void { $orderShortId = 'ABC123'; $orderData = $this->createMockCompleteOrderDTO(); @@ -177,12 +177,12 @@ public function testHandleDoesNotUpdateTicketQuantitiesForPaidOrder(): void $this->orderRepository->shouldReceive('loadRelation')->andReturnSelf(); $this->orderRepository->shouldReceive('updateFromArray')->andReturn($updatedOrder); - $this->ticketPriceRepository->shouldReceive('findWhereIn')->andReturn(new Collection([$this->createMockTicketPrice()])); + $this->productPriceRepository->shouldReceive('findWhereIn')->andReturn(new Collection([$this->createMockProductPrice()])); $this->attendeeRepository->shouldReceive('insert')->andReturn(true); - $this->attendeeRepository->shouldReceive('findWhere')->andReturn(new Collection([$this->createMockAttendee()])); + $this->attendeeRepository->shouldReceive('findWhereIn')->andReturn(new Collection([$this->createMockAttendee()])); - $this->ticketQuantityUpdateService->shouldNotReceive('updateQuantitiesFromOrder'); + $this->productQuantityUpdateService->shouldNotReceive('updateQuantitiesFromOrder'); $this->completeOrderHandler->handle($orderShortId, $orderData); @@ -202,7 +202,7 @@ public function testHandleThrowsExceptionWhenAttendeeInsertFails(): void $this->orderRepository->shouldReceive('loadRelation')->andReturnSelf(); $this->orderRepository->shouldReceive('updateFromArray')->andReturn($updatedOrder); - $this->ticketPriceRepository->shouldReceive('findWhereIn')->andReturn(new Collection([$this->createMockTicketPrice()])); + $this->productPriceRepository->shouldReceive('findWhereIn')->andReturn(new Collection([$this->createMockProductPrice()])); $this->attendeeRepository->shouldReceive('insert')->andReturn(false); @@ -225,7 +225,7 @@ public function testExceptionIsThrowWhenAttendeeCountDoesNotMatchOrderItemsCount $this->orderRepository->shouldReceive('loadRelation')->andReturnSelf(); $this->orderRepository->shouldReceive('updateFromArray')->andReturn($updatedOrder); - $this->ticketPriceRepository->shouldReceive('findWhereIn')->andReturn(new Collection([$this->createMockTicketPrice()])); + $this->productPriceRepository->shouldReceive('findWhereIn')->andReturn(new Collection([$this->createMockProductPrice()])); $this->attendeeRepository->shouldReceive('insert')->andReturn(true); $this->attendeeRepository->shouldReceive('findWhere')->andReturn(new Collection()); @@ -242,16 +242,16 @@ private function createMockCompleteOrderDTO(): CompleteOrderDTO questions: null, ); - $attendeeDTO = new CompleteOrderAttendeeDTO( + $attendeeDTO = new CompleteOrderProductDataDTO( first_name: 'John', last_name: 'Doe', email: 'john@example.com', - ticket_price_id: 1 + product_price_id: 1 ); return new CompleteOrderDTO( order: $orderDTO, - attendees: new Collection([$attendeeDTO]) + products: new Collection([$attendeeDTO]) ); } @@ -274,26 +274,26 @@ private function createMockOrderItem(): OrderItemDomainObject|MockInterface { return (new OrderItemDomainObject()) ->setId(1) - ->setTicketId(1) + ->setProductId(1) ->setQuantity(1) ->setPrice(10) ->setTotalGross(10) - ->setTicketPriceId(1); + ->setProductPriceId(1); } - private function createMockTicketPrice(): TicketPriceDomainObject|MockInterface + private function createMockProductPrice(): ProductPriceDomainObject|MockInterface { - $ticketPrice = Mockery::mock(TicketPriceDomainObject::class); - $ticketPrice->shouldReceive('getId')->andReturn(1); - $ticketPrice->shouldReceive('getTicketId')->andReturn(1); - return $ticketPrice; + $productPrice = Mockery::mock(ProductPriceDomainObject::class); + $productPrice->shouldReceive('getId')->andReturn(1); + $productPrice->shouldReceive('getProductId')->andReturn(1); + return $productPrice; } private function createMockAttendee(): AttendeeDomainObject|MockInterface { $attendee = Mockery::mock(AttendeeDomainObject::class); $attendee->shouldReceive('getId')->andReturn(1); - $attendee->shouldReceive('getTicketId')->andReturn(1); + $attendee->shouldReceive('getProductId')->andReturn(1); return $attendee; } } diff --git a/backend/tests/Unit/Services/Domain/Event/CreateEventServiceTest.php b/backend/tests/Unit/Services/Domain/Event/CreateEventServiceTest.php index fece1a14..3612cbdc 100644 --- a/backend/tests/Unit/Services/Domain/Event/CreateEventServiceTest.php +++ b/backend/tests/Unit/Services/Domain/Event/CreateEventServiceTest.php @@ -12,6 +12,7 @@ use HiEvents\Repository\Interfaces\EventStatisticRepositoryInterface; use HiEvents\Repository\Interfaces\OrganizerRepositoryInterface; use HiEvents\Services\Domain\Event\CreateEventService; +use HiEvents\Services\Domain\ProductCategory\CreateProductCategoryService; use HTMLPurifier; use Illuminate\Database\DatabaseManager; use Mockery; @@ -37,6 +38,7 @@ protected function setUp(): void $this->databaseManager = Mockery::mock(DatabaseManager::class); $this->eventStatisticsRepository = Mockery::mock(EventStatisticRepositoryInterface::class); $this->purifier = Mockery::mock(HTMLPurifier::class); + $this->createProductCategoryService = Mockery::mock(CreateProductCategoryService::class); $this->createEventService = new CreateEventService( $this->eventRepository, @@ -45,6 +47,7 @@ protected function setUp(): void $this->databaseManager, $this->eventStatisticsRepository, $this->purifier, + $this->createProductCategoryService, ); } @@ -86,16 +89,23 @@ public function testCreateEventSuccess(): void $this->eventStatisticsRepository->shouldReceive('create') ->with(Mockery::on(function ($arg) use ($eventData) { return $arg['event_id'] === $eventData->getId() && - $arg['tickets_sold'] === 0 && + $arg['products_sold'] === 0 && $arg['sales_total_gross'] === 0; })); + $this->createProductCategoryService->shouldReceive('createCategory') + ->with( + 'Tickets', + false, + Mockery::any(), + null, + 'There are no tickets available for this event.' + ); $this->purifier->shouldReceive('purify')->andReturn('Test Description'); $result = $this->createEventService->createEvent($eventData, $eventSettings); - $this->assertInstanceOf(EventDomainObject::class, $result); $this->assertEquals($eventData->getId(), $result->getId()); } @@ -121,9 +131,18 @@ public function testCreateEventWithoutEventSettings(): void $this->eventStatisticsRepository->shouldReceive('create'); - $result = $this->createEventService->createEvent($eventData); + $this->createProductCategoryService->shouldReceive('createCategory') + ->with( + 'Tickets', + false, + Mockery::any(), + null, + 'There are no tickets available for this event.' + ); - $this->assertInstanceOf(EventDomainObject::class, $result); + $this->createEventService->createEvent($eventData); + + $this->assertTrue(true); } public function testCreateEventThrowsOrganizerNotFoundException(): void diff --git a/backend/tests/Unit/Services/Domain/Order/OrderCancelServiceTest.php b/backend/tests/Unit/Services/Domain/Order/OrderCancelServiceTest.php index 76a5c1af..3ddd2415 100644 --- a/backend/tests/Unit/Services/Domain/Order/OrderCancelServiceTest.php +++ b/backend/tests/Unit/Services/Domain/Order/OrderCancelServiceTest.php @@ -12,7 +12,7 @@ use HiEvents\Repository\Interfaces\EventRepositoryInterface; use HiEvents\Repository\Interfaces\OrderRepositoryInterface; use HiEvents\Services\Domain\Order\OrderCancelService; -use HiEvents\Services\Domain\Ticket\TicketQuantityUpdateService; +use HiEvents\Services\Domain\Product\ProductQuantityUpdateService; use Illuminate\Contracts\Mail\Mailer; use Illuminate\Database\DatabaseManager; use Illuminate\Support\Collection; @@ -27,7 +27,7 @@ class OrderCancelServiceTest extends TestCase private EventRepositoryInterface $eventRepository; private OrderRepositoryInterface $orderRepository; private DatabaseManager $databaseManager; - private TicketQuantityUpdateService $ticketQuantityService; + private ProductQuantityUpdateService $productQuantityService; private OrderCancelService $service; protected function setUp(): void @@ -39,7 +39,7 @@ protected function setUp(): void $this->eventRepository = m::mock(EventRepositoryInterface::class); $this->orderRepository = m::mock(OrderRepositoryInterface::class); $this->databaseManager = m::mock(DatabaseManager::class); - $this->ticketQuantityService = m::mock(TicketQuantityUpdateService::class); + $this->productQuantityService = m::mock(ProductQuantityUpdateService::class); $this->service = new OrderCancelService( mailer: $this->mailer, @@ -47,7 +47,7 @@ protected function setUp(): void eventRepository: $this->eventRepository, orderRepository: $this->orderRepository, databaseManager: $this->databaseManager, - ticketQuantityService: $this->ticketQuantityService, + productQuantityService: $this->productQuantityService, ); } @@ -60,8 +60,8 @@ public function testCancelOrder(): void $order->shouldReceive('getLocale')->andReturn('en'); $attendees = new Collection([ - m::mock(AttendeeDomainObject::class)->shouldReceive('getTicketPriceId')->andReturn(1)->mock(), - m::mock(AttendeeDomainObject::class)->shouldReceive('getTicketPriceId')->andReturn(2)->mock(), + m::mock(AttendeeDomainObject::class)->shouldReceive('getproductPriceId')->andReturn(1)->mock(), + m::mock(AttendeeDomainObject::class)->shouldReceive('getproductPriceId')->andReturn(2)->mock(), ]); $this->attendeeRepository @@ -75,7 +75,7 @@ public function testCancelOrder(): void $this->attendeeRepository->shouldReceive('updateWhere')->once(); - $this->ticketQuantityService->shouldReceive('decreaseQuantitySold')->twice(); + $this->productQuantityService->shouldReceive('decreaseQuantitySold')->twice(); $this->orderRepository->shouldReceive('updateWhere')->once(); diff --git a/docker/development/.env b/docker/development/.env index fa5cf33e..4e500c8f 100644 --- a/docker/development/.env +++ b/docker/development/.env @@ -5,7 +5,7 @@ APP_DEBUG=true API_URL_CLIENT=https://localhost:8443/api API_URL_SERVER=http://backend:80 -STRIPE_PUBLIC_KEY=pk_test_51J3J9vJ9J9vJ9 +STRIPE_PUBLIC_KEY=pk_test_51Ofu1CJKnXOyGeQuDPUHiZcJxZozRuERiv4vQRBtCscwTbxOL574cxUjAoNRL2YLCumgC5160pl6kvTIiAc9mOeM0058KAWQ55 FRONTEND_URL=https://localhost:8443 DB_CONNECTION=pgsql @@ -13,4 +13,4 @@ DB_HOST=pgsql DB_PORT=5432 DB_DATABASE=backend DB_USERNAME=username -DB_PASSWORD=password \ No newline at end of file +DB_PASSWORD=password diff --git a/frontend/public/blank-slate/reports.svg b/frontend/public/blank-slate/reports.svg new file mode 100644 index 00000000..1eda5cdd --- /dev/null +++ b/frontend/public/blank-slate/reports.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/scripts/list_untranslated_strings.sh b/frontend/scripts/list_untranslated_strings.sh index bf8cc08f..83ad5aa7 100755 --- a/frontend/scripts/list_untranslated_strings.sh +++ b/frontend/scripts/list_untranslated_strings.sh @@ -3,7 +3,7 @@ # This script lists all untranslated strings in a .po file. # arbitrary translation file -poFile="../src/locales/es.po" +poFile="../src/locales/pt.po" if [ -f "$poFile" ]; then echo "Checking file: $poFile" diff --git a/frontend/scripts/rename.sh b/frontend/scripts/rename.sh new file mode 100755 index 00000000..84c92580 --- /dev/null +++ b/frontend/scripts/rename.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +replace_content() { + local file="$1" + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS version + sed -i '' -e 's/ticket/product/g; s/Ticket/Product/g; s/TICKET/PRODUCT/g' "$file" + else + # Linux version + sed -i 's/ticket/product/g; s/Ticket/Product/g; s/TICKET/PRODUCT/g' "$file" + fi +} + +rename_item() { + local item="$1" + local dir=$(dirname "$item") + local base=$(basename "$item") + local newbase=$(echo "$base" | sed 's/ticket/product/g; s/Ticket/Product/g; s/TICKET/PRODUCT/g') + + if [ "$base" != "$newbase" ]; then + mv "$item" "$dir/$newbase" + echo "Renamed: $item -> $dir/$newbase" + fi +} + +process_directory() { + local dir="$1" + + # First, rename directories (bottom-up to avoid path issues) + find "$dir" -depth -type d | while read -r item; do + if echo "$item" | grep -qi "ticket"; then + rename_item "$item" + fi + done + + # Then, find all files in the directory and its subdirectories + find "$dir" -type f | while read -r file; do + # Check if the file name contains "ticket" (case insensitive) + if echo "$file" | grep -qi "ticket"; then + rename_item "$file" + fi + + # Check if the file content contains "ticket" (case insensitive) + if grep -qi "ticket" "$file"; then + replace_content "$file" + echo "Modified content: $file" + fi + done +} + +if [ $# -eq 0 ]; then + echo "Usage: $0 " + exit 1 +fi + +if [ ! -d "$1" ]; then + echo "Error: $1 is not a directory" + exit 1 +fi + +process_directory "$1" + +# Remove any leftover -e files (backup files created by sed on some systems) +find "$1" -name "*-e" -type f -delete + +echo "Renaming and replacement complete." +echo "Removed any leftover -e backup files." diff --git a/frontend/src/api/api-keys.client.ts b/frontend/src/api/api-keys.client.ts new file mode 100644 index 00000000..16683bab --- /dev/null +++ b/frontend/src/api/api-keys.client.ts @@ -0,0 +1,17 @@ +import {api} from "./client.ts"; +import {GenericDataResponse, IdParam, CreateApiKeyReques, ApiKey} from "../types.ts"; + +export const apiKeysClient = { + create: async (request: CreateApiKeyRequest) => { + const response = await api.post>(`api-keys`, request); + return response.data; + }, + all: async () => { + const response = await api.get>(`api-keys`); + return response.data; + }, + revoke: async (tokenId: IdParam) => { + const response = await api.delete>(`api-keys/${tokenId}`); + return response.data; + }, +} \ No newline at end of file diff --git a/frontend/src/api/attendee.client.ts b/frontend/src/api/attendee.client.ts index c420ea50..ba44fb83 100644 --- a/frontend/src/api/attendee.client.ts +++ b/frontend/src/api/attendee.client.ts @@ -8,8 +8,9 @@ export interface EditAttendeeRequest { first_name: string; last_name: string; email: string; - ticket_id?: IdParam; - ticket_price_id?: IdParam; + notes?: string; + product_id?: IdParam; + product_price_id?: IdParam; status?: string; } @@ -72,4 +73,4 @@ export const attendeeClientPublic = { const response = await publicApi.get>>(`events/${eventId}/attendees/${attendeeShortId}`); return response.data; }, -} \ No newline at end of file +} diff --git a/frontend/src/api/auth.client.ts b/frontend/src/api/auth.client.ts index f2de5fab..40636151 100644 --- a/frontend/src/api/auth.client.ts +++ b/frontend/src/api/auth.client.ts @@ -20,7 +20,9 @@ export const authClient = { }, login: async (user: LoginData) => { - const response = await api.post('auth/login', user); + const response = await api.get('/csrf-cookie').then(async response => { + return api.post('auth/login', user); + }); return response.data; }, diff --git a/frontend/src/api/capacity-assignment.client.ts b/frontend/src/api/capacity-assignment.client.ts index 2819cbbf..43a324e7 100644 --- a/frontend/src/api/capacity-assignment.client.ts +++ b/frontend/src/api/capacity-assignment.client.ts @@ -4,7 +4,8 @@ import { CapacityAssignmentRequest, GenericDataResponse, GenericPaginatedResponse, - IdParam, QueryFilters, + IdParam, + QueryFilters, } from "../types"; import {queryParamsHelper} from "../utilites/queryParamsHelper.ts"; diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 67342e3c..604730b6 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -22,8 +22,9 @@ const ALLOWED_UNAUTHENTICATED_PATHS = [ 'print', '/order/', 'widget', - '/ticket/', + '/product/', 'check-in', + 'csrf-cookie', ]; export const api = axios.create({ @@ -32,6 +33,7 @@ export const api = axios.create({ 'Content-Type': 'application/json' }, withCredentials: true, + withXSRFToken: true, }); const existingToken = typeof window !== "undefined" ? window.localStorage.getItem('token') : undefined; diff --git a/frontend/src/api/event.client.ts b/frontend/src/api/event.client.ts index 41ad35a2..4d7ad46c 100644 --- a/frontend/src/api/event.client.ts +++ b/frontend/src/api/event.client.ts @@ -82,6 +82,11 @@ export const eventsClient = { status }); return response.data; + }, + + getEventReport: async (eventId: IdParam, reportType: IdParam, startDate?: string, endDate?: string) => { + const response = await api.get>('events/' + eventId + '/reports/' + reportType + '?start_date=' + startDate + '&end_date=' + endDate); + return response.data; } } diff --git a/frontend/src/api/order.client.ts b/frontend/src/api/order.client.ts index 9ee16ec5..a639ada6 100644 --- a/frontend/src/api/order.client.ts +++ b/frontend/src/api/order.client.ts @@ -17,7 +17,7 @@ export interface OrderDetails { } export interface AttendeeDetails extends OrderDetails { - ticket_id: number, + product_id: number, } export interface FinaliseOrderPayload { @@ -26,19 +26,19 @@ export interface FinaliseOrderPayload { } -export interface TicketPriceQuantityFormValue { +export interface ProductPriceQuantityFormValue { price?: number, quantity: number, price_id: number, } -export interface TicketFormValue { - ticket_id: number, - quantities: TicketPriceQuantityFormValue[], +export interface ProductFormValue { + product_id: number, + quantities: ProductPriceQuantityFormValue[], } -export interface TicketFormPayload { - tickets?: TicketFormValue[], +export interface ProductFormPayload { + products?: ProductFormValue[], promo_code: string | null, session_identifier?: string, } @@ -88,7 +88,7 @@ export const orderClient = { } export const orderClientPublic = { - create: async (eventId: number, createOrderPayload: TicketFormPayload) => { + create: async (eventId: number, createOrderPayload: ProductFormPayload) => { const response = await publicApi.post>('events/' + eventId + '/order', createOrderPayload); return response.data; }, diff --git a/frontend/src/api/product-category.client.ts b/frontend/src/api/product-category.client.ts new file mode 100644 index 00000000..38489384 --- /dev/null +++ b/frontend/src/api/product-category.client.ts @@ -0,0 +1,45 @@ +import { api } from "./client"; +import { + ProductCategory, + GenericDataResponse, + IdParam, +} from "../types"; + +export const productCategoryClient = { + create: async (eventId: IdParam, productCategory: ProductCategory) => { + const response = await api.post>( + `events/${eventId}/product-categories`, + productCategory + ); + return response.data; + }, + + update: async (eventId: IdParam, productCategoryId: IdParam, productCategory: ProductCategory) => { + const response = await api.put>( + `events/${eventId}/product-categories/${productCategoryId}`, + productCategory + ); + return response.data; + }, + + all: async (eventId: IdParam) => { + const response = await api.get>( + `events/${eventId}/product-categories` + ); + return response.data; + }, + + get: async (eventId: IdParam, productCategoryId: IdParam) => { + const response = await api.get>( + `events/${eventId}/product-categories/${productCategoryId}` + ); + return response.data; + }, + + delete: async (eventId: IdParam, productCategoryId: IdParam) => { + const response = await api.delete>( + `events/${eventId}/product-categories/${productCategoryId}` + ); + return response.data; + }, +}; diff --git a/frontend/src/api/product.client.ts b/frontend/src/api/product.client.ts new file mode 100644 index 00000000..ea8b9e5b --- /dev/null +++ b/frontend/src/api/product.client.ts @@ -0,0 +1,48 @@ +import {api} from "./client"; +import { + GenericDataResponse, + GenericPaginatedResponse, + IdParam, + QueryFilters, SortableItem, + Product, +} from "../types"; +import {queryParamsHelper} from "../utilites/queryParamsHelper.ts"; +import {publicApi} from "./public-client.ts"; + +export const productClient = { + findById: async (eventId: IdParam, productId: IdParam) => { + const response = await api.get>(`/events/${eventId}/products/${productId}`); + return response.data; + }, + all: async (eventId: IdParam, pagination: QueryFilters) => { + const response = await api.get>( + `/events/${eventId}/products` + queryParamsHelper.buildQueryString(pagination) + ); + return response.data; + }, + create: async (eventId: IdParam, product: Product) => { + const response = await api.post>(`events/${eventId}/products`, product); + return response.data; + }, + update: async (eventId: IdParam, productId: IdParam, product: Product) => { + const response = await api.put>(`events/${eventId}/products/${productId}`, product); + return response.data; + }, + delete: async (eventId: IdParam, productId: IdParam) => { + const response = await api.delete>(`/events/${eventId}/products/${productId}`); + return response.data; + }, + sortAllProducts: async (eventId: IdParam, sortedCategories: { product_category_id: IdParam, sorted_products: SortableItem[] }[]) => { + return await api.post(`/events/${eventId}/products/sort`, { + 'sorted_categories': sortedCategories, + }); + } +} + +export const productClientPublic = { + findByEventId: async (eventId: IdParam) => { + const response = await publicApi.get>(`/events/${eventId}/products`); + return response.data; + }, +} + diff --git a/frontend/src/api/ticket.client.ts b/frontend/src/api/ticket.client.ts deleted file mode 100644 index 3d1f6e1a..00000000 --- a/frontend/src/api/ticket.client.ts +++ /dev/null @@ -1,46 +0,0 @@ -import {api} from "./client"; -import { - GenericDataResponse, - GenericPaginatedResponse, - IdParam, - QueryFilters, SortableItem, - Ticket, -} from "../types"; -import {queryParamsHelper} from "../utilites/queryParamsHelper.ts"; -import {publicApi} from "./public-client.ts"; - -export const ticketClient = { - findById: async (eventId: IdParam, ticketId: IdParam) => { - const response = await api.get>(`/events/${eventId}/tickets/${ticketId}`); - return response.data; - }, - all: async (eventId: IdParam, pagination: QueryFilters) => { - const response = await api.get>( - `/events/${eventId}/tickets` + queryParamsHelper.buildQueryString(pagination) - ); - return response.data; - }, - create: async (eventId: IdParam, ticket: Ticket) => { - const response = await api.post>(`events/${eventId}/tickets`, ticket); - return response.data; - }, - update: async (eventId: IdParam, ticketId: IdParam, ticket: Ticket) => { - const response = await api.put>(`events/${eventId}/tickets/${ticketId}`, ticket); - return response.data; - }, - delete: async (eventId: IdParam, ticketId: IdParam) => { - const response = await api.delete>(`/events/${eventId}/tickets/${ticketId}`); - return response.data; - }, - sortTickets: async (eventId: IdParam, ticketSort: SortableItem[]) => { - return await api.post(`/events/${eventId}/tickets/sort`, ticketSort); - } -} - -export const ticketClientPublic = { - findByEventId: async (eventId: IdParam) => { - const response = await publicApi.get>(`/events/${eventId}/tickets`); - return response.data; - }, -} - diff --git a/frontend/src/components/common/Accordion/Accordion.module.scss b/frontend/src/components/common/Accordion/Accordion.module.scss new file mode 100644 index 00000000..a195483f --- /dev/null +++ b/frontend/src/components/common/Accordion/Accordion.module.scss @@ -0,0 +1,54 @@ +.accordionItem { + border: 1px solid var(--mantine-color-gray-2); + border-radius: var(--mantine-radius-sm); + background-color: var(--mantine-color-white); + overflow: hidden; + + & + & { + margin-top: 0.75rem; + } +} + +.accordionControl { + background-color: var(--mantine-color-gray-0); + border-bottom: none; + + &:hover { + background-color: var(--mantine-color-gray-1); + } + + &[data-expanded] { + background-color: var(--mantine-color-white); + border-bottom: 1px solid var(--mantine-color-gray-2); + } +} + +.accordionContent { + padding: 1.25rem; + + @media (max-width: 768px) { + padding: 1rem; + } +} + +.accordionChevron { + transition: transform 0.2s ease; + + &[data-expanded] { + transform: rotate(180deg); + } +} + +.header { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.title { + flex: 1; +} + +.badge { + margin-left: auto; +} diff --git a/frontend/src/components/common/Accordion/index.tsx b/frontend/src/components/common/Accordion/index.tsx new file mode 100644 index 00000000..7662a325 --- /dev/null +++ b/frontend/src/components/common/Accordion/index.tsx @@ -0,0 +1,51 @@ +import {Accordion as MantineAccordion, Group, Text} from '@mantine/core'; +import {TablerIconsProps} from '@tabler/icons-react'; +import classes from './Accordion.module.scss'; +import React from "react"; + +export interface AccordionItem { + value: string; + icon?: (props: TablerIconsProps) => JSX.Element; + title: string; + count?: number; + content: React.ReactNode; +} + +interface AccordionProps { + items: AccordionItem[]; + defaultValue?: string; +} + +export const Accordion = ({items, defaultValue}: AccordionProps) => { + return ( + + {items.map((item) => ( + + + + {item.icon && } + {item.title} + {item.count !== undefined && ( + + ({item.count}) + + )} + + + + {item.content} + + + ))} + + ); +}; diff --git a/frontend/src/components/common/AddEventToCalendarButton/index.tsx b/frontend/src/components/common/AddEventToCalendarButton/index.tsx new file mode 100644 index 00000000..d2cd5cb2 --- /dev/null +++ b/frontend/src/components/common/AddEventToCalendarButton/index.tsx @@ -0,0 +1,143 @@ +import {ActionIcon, Button, Popover, Stack, Text, Tooltip} from '@mantine/core'; +import {IconBrandGoogle, IconCalendarPlus, IconDownload} from '@tabler/icons-react'; +import {t} from "@lingui/macro"; + +interface LocationDetails { + venue_name?: string; + + [key: string]: any; +} + +interface EventSettings { + location_details?: LocationDetails; +} + +interface Event { + title: string; + description_preview?: string; + description?: string; + start_date: string; + end_date?: string; + settings?: EventSettings; +} + +interface AddToCalendarProps { + event: Event; +} + +const eventLocation = (event: Event): string => { + if (event.settings?.location_details) { + const details = event.settings.location_details; + const addressParts = []; + + if (details.street_address) addressParts.push(details.street_address); + if (details.street_address_2) addressParts.push(details.street_address_2); + if (details.city) addressParts.push(details.city); + if (details.state) addressParts.push(details.state); + if (details.postal_code) addressParts.push(details.postal_code); + if (details.country) addressParts.push(details.country); + + const address = addressParts.join(', '); + + if (details.venue_name) { + return `${details.venue_name}, ${address}`; + } + + return address; + } + + return ''; +}; + +const createICSContent = (event: Event): string => { + const formatDate = (date: string): string => { + return new Date(date).toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, ''); + }; + + const stripHtml = (html: string): string => { + const tmp = document.createElement('div'); + tmp.innerHTML = html || ''; + return tmp.textContent || tmp.innerText || ''; + }; + + return [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//Hi.Events//NONSGML Event Calendar//EN', + 'CALSCALE:GREGORIAN', + 'BEGIN:VEVENT', + `DTSTART:${formatDate(event.start_date)}`, + `DTEND:${formatDate(event.end_date || event.start_date)}`, + `SUMMARY:${event.title.replace(/\n/g, '\\n')}`, + `DESCRIPTION:${stripHtml(event.description_preview || '').replace(/\n/g, '\\n')}`, + `LOCATION:${eventLocation(event)}`, + `DTSTAMP:${formatDate(new Date().toISOString())}`, + `UID:${crypto.randomUUID()}@hi.events`, + 'END:VEVENT', + 'END:VCALENDAR' + ].join('\r\n'); +}; + +const downloadICSFile = (event: Event): void => { + const content = createICSContent(event); + const blob = new Blob([content], {type: 'text/calendar;charset=utf-8'}); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.setAttribute('download', `${event.title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.ics`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +}; + +const createGoogleCalendarUrl = (event: Event): string => { + const formatGoogleDate = (date: string): string => { + return new Date(date).toISOString().replace(/-|:|\.\d{3}/g, ''); + }; + + const params = new URLSearchParams({ + action: 'TEMPLATE', + text: event.title, + details: event.description_preview || '', + location: eventLocation(event), + dates: `${formatGoogleDate(event.start_date)}/${formatGoogleDate(event.end_date || event.start_date)}` + }); + + return `https://calendar.google.com/calendar/render?${params.toString()}`; +}; + +export const AddToEventCalendarButton = ({event}: AddToCalendarProps) => { + return ( + + + + + + + + + + + {t`Add to Calendar`} + + + + + + ); +}; diff --git a/frontend/src/components/common/AttendeeDetails/index.tsx b/frontend/src/components/common/AttendeeDetails/index.tsx index 414bca71..92becd73 100644 --- a/frontend/src/components/common/AttendeeDetails/index.tsx +++ b/frontend/src/components/common/AttendeeDetails/index.tsx @@ -1,14 +1,13 @@ import {Anchor} from "@mantine/core"; -import {Card} from "../Card"; import {Attendee} from "../../../types.ts"; import classes from "./AttendeeDetails.module.scss"; import {t} from "@lingui/macro"; -import {getAttendeeTicketTitle} from "../../../utilites/tickets.ts"; +import {getAttendeeProductTitle} from "../../../utilites/products.ts"; import {getLocaleName, SupportedLocales} from "../../../locales.ts"; export const AttendeeDetails = ({attendee}: { attendee: Attendee }) => { return ( - +
{t`Name`} @@ -30,7 +29,7 @@ export const AttendeeDetails = ({attendee}: { attendee: Attendee }) => { {t`Status`}
- {attendee.status} + {attendee.status === 'ACTIVE' ? {t`Active`} : {t`Canceled`}}
@@ -43,10 +42,10 @@ export const AttendeeDetails = ({attendee}: { attendee: Attendee }) => {
- {t`Ticket`} + {t`Product`}
- {getAttendeeTicketTitle(attendee)} + {getAttendeeProductTitle(attendee)}
@@ -57,6 +56,6 @@ export const AttendeeDetails = ({attendee}: { attendee: Attendee }) => { {getLocaleName(attendee.locale as SupportedLocales)}
-
+ ); } diff --git a/frontend/src/components/common/AttendeeList/AttendeeList.module.scss b/frontend/src/components/common/AttendeeList/AttendeeList.module.scss index 008a7faa..622146ad 100644 --- a/frontend/src/components/common/AttendeeList/AttendeeList.module.scss +++ b/frontend/src/components/common/AttendeeList/AttendeeList.module.scss @@ -1,35 +1,60 @@ +.container { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + .attendeeList { - margin-bottom: var(--tk-spacing-lg); - - .attendee { - display: flex; - padding: 10px; - border: 1px solid #dddddd; - border-radius: 5px; - margin-bottom: 10px; - align-items: center; - - &:last-of-type { - margin-bottom: 0; - } - - .attendeeName { - margin-left: 10px; - - .ticketName { - color: #9ca3af; - font-size: .8em; - } - } - - .viewAttendee { - flex: 1; - display: flex; - place-content: flex-end; - - a { - align-self: flex-end; - } - } + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.attendee { + display: flex; + align-items: center; + padding: 0.875rem; + background: var(--mantine-color-gray-0); + border: 1px solid var(--mantine-color-gray-2); + border-radius: var(--mantine-radius-sm); + transition: all 0.2s ease; + + &:hover { + border-color: var(--mantine-color-gray-3); + transform: translateY(-1px); } -} \ No newline at end of file +} + +.attendeeInfo { + display: flex; + align-items: center; + gap: 1rem; + flex: 1; +} + +.details { + flex: 1; + min-width: 0; +} + +.name { + line-height: 1.2; +} + +.product { + margin-top: 0.25rem; + color: var(--mantine-color-gray-6); +} + +.actionButton { + margin-left: auto; + + &:hover { + background-color: var(--mantine-color-blue-0); + } +} + +.avatar { + background: var(--tk-color-gray); + font-weight: 500; +} diff --git a/frontend/src/components/common/AttendeeList/index.tsx b/frontend/src/components/common/AttendeeList/index.tsx index a34f5f43..0f3f35f0 100644 --- a/frontend/src/components/common/AttendeeList/index.tsx +++ b/frontend/src/components/common/AttendeeList/index.tsx @@ -1,38 +1,79 @@ -import {ActionIcon, Avatar, Tooltip} from "@mantine/core"; -import {getInitials} from "../../../utilites/helpers.ts"; -import Truncate from "../Truncate"; -import {NavLink} from "react-router-dom"; -import {IconEye} from "@tabler/icons-react"; +import { ActionIcon, Avatar, Tooltip, Text, Group } from "@mantine/core"; +import { getInitials } from "../../../utilites/helpers.ts"; +import { NavLink } from "react-router-dom"; +import { IconExternalLink, IconUsers } from "@tabler/icons-react"; import classes from './AttendeeList.module.scss'; -import {Order, Ticket} from "../../../types.ts"; -import {t} from "@lingui/macro"; +import { Order, Product } from "../../../types.ts"; +import { t } from "@lingui/macro"; + +interface AttendeeListProps { + order: Order; + products: Product[]; +} + +export const AttendeeList = ({ order, products }: AttendeeListProps) => { + const attendeeCount = order.attendees?.length || 0; + + if (!order.attendees?.length) { + return ( +
+ + {t`No attendees found for this order.`} + +
+ ); + } -export const AttendeeList = ({order, tickets}: { order: Order, tickets: Ticket[] }) => { return ( -
- {order.attendees?.map(attendee => ( -
- - {getInitials(attendee.first_name + ' ' + attendee.last_name)} - +
+
+ {order.attendees.map(attendee => { + const product = products?.find(p => p.id === attendee.product_id); + const fullName = `${attendee.first_name} ${attendee.last_name}`; -
- {attendee.first_name + ' ' + attendee.last_name} -
- ticket.id === attendee.ticket_id)?.title}/> + return ( +
+
+ + {getInitials(fullName)} + + +
+ + {fullName} + + {product?.title && ( + + {product.title} + + )} +
+ + + + + + + + +
-
-
- - - - - - - -
-
- ))} + ); + })} +
- ) -} + ); +}; diff --git a/frontend/src/components/common/AttendeeTicket/AttendeeTicket.module.scss b/frontend/src/components/common/AttendeeProduct/AttendeeProduct.module.scss similarity index 96% rename from frontend/src/components/common/AttendeeTicket/AttendeeTicket.module.scss rename to frontend/src/components/common/AttendeeProduct/AttendeeProduct.module.scss index 3dd82050..3605fb8f 100644 --- a/frontend/src/components/common/AttendeeTicket/AttendeeTicket.module.scss +++ b/frontend/src/components/common/AttendeeProduct/AttendeeProduct.module.scss @@ -43,13 +43,13 @@ flex: 1; } - .ticketName { + .productName { font-size: 0.9em; font-weight: 900; margin-bottom: 5px; } - .ticketPrice { + .productPrice { .badge { background-color: #8BC34A; color: #fff; @@ -114,7 +114,7 @@ } } - .ticketButtons { + .productButtons { background: #ffffff; border-radius: 5px; margin-top: 20px; diff --git a/frontend/src/components/common/AttendeeTicket/index.tsx b/frontend/src/components/common/AttendeeProduct/index.tsx similarity index 74% rename from frontend/src/components/common/AttendeeTicket/index.tsx rename to frontend/src/components/common/AttendeeProduct/index.tsx index 823ca932..e5802f9e 100644 --- a/frontend/src/components/common/AttendeeTicket/index.tsx +++ b/frontend/src/components/common/AttendeeProduct/index.tsx @@ -1,23 +1,23 @@ import {Card} from "../Card"; -import {getAttendeeTicketPrice, getAttendeeTicketTitle} from "../../../utilites/tickets.ts"; +import {getAttendeeProductPrice, getAttendeeProductTitle} from "../../../utilites/products.ts"; import {Anchor, Button, CopyButton} from "@mantine/core"; import {formatCurrency} from "../../../utilites/currency.ts"; import {t} from "@lingui/macro"; import {prettyDate} from "../../../utilites/dates.ts"; import QRCode from "react-qr-code"; import {IconCopy, IconPrinter} from "@tabler/icons-react"; -import {Attendee, Event, Ticket} from "../../../types.ts"; -import classes from './AttendeeTicket.module.scss'; +import {Attendee, Event, Product} from "../../../types.ts"; +import classes from './AttendeeProduct.module.scss'; -interface AttendeeTicketProps { +interface AttendeeProductProps { event: Event; attendee: Attendee; - ticket: Ticket; + product: Product; hideButtons?: boolean; } -export const AttendeeTicket = ({attendee, ticket, event, hideButtons = false}: AttendeeTicketProps) => { - const ticketPrice = getAttendeeTicketPrice(attendee, ticket); +export const AttendeeProduct = ({attendee, product, event, hideButtons = false}: AttendeeProductProps) => { + const productPrice = getAttendeeProductPrice(attendee, product); return ( @@ -27,17 +27,17 @@ export const AttendeeTicket = ({attendee, ticket, event, hideButtons = false}: A

{attendee.first_name} {attendee.last_name}

-
- {getAttendeeTicketTitle(attendee)} +
+ {getAttendeeProductTitle(attendee)}
{attendee.email}
-
+
- {ticketPrice > 0 && formatCurrency(ticketPrice, event?.currency)} - {ticketPrice === 0 && t`Free`} + {productPrice > 0 && formatCurrency(productPrice, event?.currency)} + {productPrice === 0 && t`Free`}
@@ -65,16 +65,16 @@ export const AttendeeTicket = ({attendee, ticket, event, hideButtons = false}: A
{!hideButtons && ( -
+
- + {({copied, copy}) => ( + ); +}; diff --git a/frontend/src/components/common/EventCard/index.tsx b/frontend/src/components/common/EventCard/index.tsx index 1ca52bf2..602c63b2 100644 --- a/frontend/src/components/common/EventCard/index.tsx +++ b/frontend/src/components/common/EventCard/index.tsx @@ -93,7 +93,7 @@ export function EventCard({event}: EventCardProps) {
{event && }
- + {event.title}
@@ -123,7 +123,7 @@ export function EventCard({event}: EventCardProps) {
- {formatNumber(event?.statistics?.tickets_sold || 0)} {t`tickets sold`} + {formatNumber(event?.statistics?.products_sold || 0)} {t`products sold`}
diff --git a/frontend/src/components/common/EventDocumentHead/index.tsx b/frontend/src/components/common/EventDocumentHead/index.tsx index 2c693d94..7bb2bacd 100644 --- a/frontend/src/components/common/EventDocumentHead/index.tsx +++ b/frontend/src/components/common/EventDocumentHead/index.tsx @@ -10,6 +10,7 @@ interface EventDocumentHeadProps { export const EventDocumentHead = ({event}: EventDocumentHeadProps) => { const eventSettings = event.settings; + const products = event.product_categories?.flatMap(category => category.products) ?? []; const title = (eventSettings?.seo_title ?? event.title) + ' | ' + event.organizer?.name; const description = eventSettings?.seo_description ?? event.description_preview; const keywords = eventSettings?.seo_keywords; @@ -57,13 +58,13 @@ export const EventDocumentHead = ({event}: EventDocumentHeadProps) => { eventStatus: 'https://schema.org/EventScheduled', eventAttendanceMode: event.settings?.is_online_event ? "https://schema.org/OnlineEventAttendanceMode" : "https://schema.org/OfflineEventAttendanceMode", currency: event.currency, - offers: event.tickets?.map(ticket => ({ + offers: products.map(product => ({ "@type": "http://schema.org/Offer", url, - price: ticket.prices?.[0]?.price, + price: product?.prices?.[0]?.price, priceCurrency: event.currency, validFrom: startDate, - availability: ticket.is_available ? "http://schema.org/InStock" : "http://schema.org/SoldOut", + availability: product?.is_available ? "http://schema.org/InStock" : "http://schema.org/SoldOut", })), }; diff --git a/frontend/src/components/common/HomepageInfoMessage/index.tsx b/frontend/src/components/common/HomepageInfoMessage/index.tsx index 97e20b72..8e29937c 100644 --- a/frontend/src/components/common/HomepageInfoMessage/index.tsx +++ b/frontend/src/components/common/HomepageInfoMessage/index.tsx @@ -33,4 +33,4 @@ export const HomepageInfoMessage = ({message, link, linkText, iconType = 'info'} )}
); -} \ No newline at end of file +} diff --git a/frontend/src/components/common/MessageList/index.tsx b/frontend/src/components/common/MessageList/index.tsx index f45008f2..17979101 100644 --- a/frontend/src/components/common/MessageList/index.tsx +++ b/frontend/src/components/common/MessageList/index.tsx @@ -61,7 +61,7 @@ export const MessageList = ({messages}: MessageListProps) => { subHeading={( <>

- {t`You haven't sent any messages yet. You can send messages to all attendees, or to specific ticket holders.`} + {t`You haven't sent any messages yet. You can send messages to all attendees, or to specific product holders.`}

)} diff --git a/frontend/src/components/common/NumberSelector/index.tsx b/frontend/src/components/common/NumberSelector/index.tsx index 4c795dbb..579e3658 100644 --- a/frontend/src/components/common/NumberSelector/index.tsx +++ b/frontend/src/components/common/NumberSelector/index.tsx @@ -29,12 +29,12 @@ export const NumberSelector = ({formInstance, fieldName, min, max, sharedValues} }, [value]); useEffect(() => { - // to handle application promo code after updating the quanity + // to handle application promo code after updating the quantity const formValue = _.get(formInstance.values, fieldName) if (formValue !== value) { formInstance.setFieldValue(fieldName, value); } - }, [formInstance]); + }, [formInstance.values]); const increment = () => { // Adjust from 0 to minValue on the first increment, if minValue is greater than 0 diff --git a/frontend/src/components/common/OnlineEventDetails/index.tsx b/frontend/src/components/common/OnlineEventDetails/index.tsx index d40a1b92..b428316c 100644 --- a/frontend/src/components/common/OnlineEventDetails/index.tsx +++ b/frontend/src/components/common/OnlineEventDetails/index.tsx @@ -5,7 +5,7 @@ import {EventSettings} from "../../../types.ts"; export const OnlineEventDetails = (props: { eventSettings: EventSettings }) => { return <> {(props.eventSettings.is_online_event && props.eventSettings.online_event_connection_details) && ( -
+

{t`Online Event Details`}

{ +export const OrderDetails = ({order, event, cardVariant = 'lightGray'}: { + order: Order, + event: Event, + cardVariant?: CardVariant +}) => { return ( - +
{t`Name`} @@ -43,7 +47,7 @@ export const OrderDetails = ({order, event}: { order: Order, event: Event }) => {t`Status`}
- +
@@ -64,4 +68,4 @@ export const OrderDetails = ({order, event}: { order: Order, event: Event }) =>
); -} \ No newline at end of file +} diff --git a/frontend/src/components/common/PageTitle/index.tsx b/frontend/src/components/common/PageTitle/index.tsx index 1c3db165..e8314e01 100644 --- a/frontend/src/components/common/PageTitle/index.tsx +++ b/frontend/src/components/common/PageTitle/index.tsx @@ -1,12 +1,14 @@ import React from "react"; import classes from './PageTitle.module.scss'; -interface PageTitleProps { +interface PageTitleProps extends React.HTMLAttributes { children: React.ReactNode, } -export const PageTitle = ({children}: PageTitleProps) => { +export const PageTitle = (props: PageTitleProps) => { return ( -

{children}

+

+ {props.children} +

); } diff --git a/frontend/src/components/common/PoweredByFooter/index.tsx b/frontend/src/components/common/PoweredByFooter/index.tsx index c3b3693e..92ab0777 100644 --- a/frontend/src/components/common/PoweredByFooter/index.tsx +++ b/frontend/src/components/common/PoweredByFooter/index.tsx @@ -29,7 +29,7 @@ export const PoweredByFooter = (props: React.DetailedHTMLProps + title={'Effortlessly manage events and sell products online with Hi.Events'}> Hi.Events 🚀
diff --git a/frontend/src/components/common/TicketPriceAvailability/index.tsx b/frontend/src/components/common/ProductPriceAvailability/index.tsx similarity index 51% rename from frontend/src/components/common/TicketPriceAvailability/index.tsx rename to frontend/src/components/common/ProductPriceAvailability/index.tsx index c5b84762..b32f1ece 100644 --- a/frontend/src/components/common/TicketPriceAvailability/index.tsx +++ b/frontend/src/components/common/ProductPriceAvailability/index.tsx @@ -1,10 +1,10 @@ -import {Event, Ticket, TicketPrice} from "../../../types.ts"; +import {Event, Product, ProductPrice} from "../../../types.ts"; import {t} from "@lingui/macro"; import {Tooltip} from "@mantine/core"; import {prettyDate, relativeDate} from "../../../utilites/dates.ts"; import {IconInfoCircle} from "@tabler/icons-react"; -const TicketPriceSaleDateMessage = ({price, event}: { price: TicketPrice, event: Event }) => { +const ProductPriceSaleDateMessage = ({price, event}: { price: ProductPrice, event: Event }) => { if (price.is_sold_out) { return t`Sold out`; } @@ -27,19 +27,19 @@ const TicketPriceSaleDateMessage = ({price, event}: { price: TicketPrice, event: return t`Not available`; } -export const TicketAvailabilityMessage = ({ticket, event}: { ticket: Ticket, event: Event }) => { - if (ticket.is_sold_out) { +export const ProductAvailabilityMessage = ({product, event}: { product: Product, event: Event }) => { + if (product.is_sold_out) { return t`Sold out`; } - if (ticket.is_after_sale_end_date) { + if (product.is_after_sale_end_date) { return t`Sales ended`; } - if (ticket.is_before_sale_start_date) { + if (product.is_before_sale_start_date) { return ( {t`Sales start`}{' '} - - {relativeDate(String(ticket.sale_start_date))}{' '} + + {relativeDate(String(product.sale_start_date))}{' '} ); @@ -48,17 +48,17 @@ export const TicketAvailabilityMessage = ({ticket, event}: { ticket: Ticket, eve return t`Not available`; } -interface TicketAndPriceAvailabilityProps { - ticket: Ticket; - price: TicketPrice; +interface ProductAndPriceAvailabilityProps { + product: Product; + price: ProductPrice; event: Event; } -export const TicketPriceAvailability = ({ticket, price, event}: TicketAndPriceAvailabilityProps) => { +export const ProductPriceAvailability = ({product, price, event}: ProductAndPriceAvailabilityProps) => { - if (ticket.type === 'TIERED') { - return + if (product.type === 'TIERED') { + return } - return + return } diff --git a/frontend/src/components/common/ProductSelector/index.tsx b/frontend/src/components/common/ProductSelector/index.tsx new file mode 100644 index 00000000..23857bef --- /dev/null +++ b/frontend/src/components/common/ProductSelector/index.tsx @@ -0,0 +1,96 @@ +import {MultiSelect, Select} from "@mantine/core"; +import {IconTicket} from "@tabler/icons-react"; +import {UseFormReturnType} from "@mantine/form"; +import {ProductCategory, ProductType} from "../../../types.ts"; +import React from "react"; +import {t} from "@lingui/macro"; + +interface ProductSelectorProps { + label: string; + placeholder: string; + icon?: React.ReactNode; + productCategories: ProductCategory[]; + form: UseFormReturnType; + productFieldName: string; + tierFieldName?: string; + includedProductTypes?: ProductType[]; + multiSelect?: boolean; + showTierSelector?: boolean; +} + +export const ProductSelector = ({ + label, + placeholder, + icon = , + productCategories, + form, + productFieldName, + tierFieldName = 'product_price_id', + includedProductTypes = [ProductType.Ticket, ProductType.General], + multiSelect = true, + showTierSelector = false, + }: ProductSelectorProps) => { + const formattedData = productCategories?.map((category) => ({ + group: category.name, + items: + category.products + ?.filter((product) => includedProductTypes.includes(product.product_type)) + ?.map((product) => ({ + value: String(product.id), + label: product.title, + })) || [], + })); + const eventProducts = productCategories?.flatMap(category => category.products).filter(product => product !== undefined); + + const TierSelector = () => { + return ( + <> + {eventProducts?.find(product => product.id == form.values.product_id)?.type === 'TIERED' && ( + + {showTierSelector && } + + + ); + } +}; diff --git a/frontend/src/components/common/ProductsTable/ProductsBlankSlate/index.tsx b/frontend/src/components/common/ProductsTable/ProductsBlankSlate/index.tsx new file mode 100644 index 00000000..7dcda08b --- /dev/null +++ b/frontend/src/components/common/ProductsTable/ProductsBlankSlate/index.tsx @@ -0,0 +1,72 @@ +import {NoResultsSplash} from "../../NoResultsSplash"; +import {Button} from "../../Button"; +import {IconPlus} from "@tabler/icons-react"; +import {t, Trans} from "@lingui/macro"; + +interface ProductsBlankSlateProps { + openCreateModal: (categoryId?: string) => void; + productCategories: any; + searchTerm: string; +} + +export const ProductsBlankSlate = ({openCreateModal, productCategories, searchTerm}: ProductsBlankSlateProps) => { + const showLargeBlankSlate = productCategories + .every((category: any) => category.products.length === 0) && productCategories.length === 1; + + if (searchTerm) { + return ( + +

+ + We couldn't find any tickets matching {searchTerm ? + {searchTerm} : 'your search'} + +

+ + )} + /> + ); + } + + if (showLargeBlankSlate) { + return ( + +

+ {t`You'll need at least one product to get started. Free, paid or let the user decide what to pay.`} +

+ + + )} + /> + ); + } + + return ( +

+ {t`This category doesn't have any products yet.`} +

+ +
+ ) +} diff --git a/frontend/src/components/common/ProductsTable/ProductsTable.module.scss b/frontend/src/components/common/ProductsTable/ProductsTable.module.scss new file mode 100644 index 00000000..11c764ac --- /dev/null +++ b/frontend/src/components/common/ProductsTable/ProductsTable.module.scss @@ -0,0 +1,256 @@ +@import "../../../styles/mixins.scss"; + +.sortableCategory { + margin-bottom: 20px; + transition: transform 250ms ease; + border-radius: var(--tk-radius-sm); + padding: var(--tk-spacing-lg); + box-shadow: 0 3px 0 #dddddd; + border: 1px solid #e3e3e3; + background-color: #FFFFFF; +} + +.categoryHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; + gap: 10px; +} + +.categoryActions { + display: flex; + gap: 5px; +} + +.categoryAction { + margin-left: auto; +} + +.categoryTitle { + margin: 0; + display: flex; + align-items: center; + gap: 10px; +} + +.categoryDragHandle { + cursor: grab; + + &:active { + cursor: grabbing; + } +} + +.dragHandle { + touch-action: none; + margin-top: 5px; +} + +.dragHandleDisabled { + cursor: not-allowed; + opacity: 0.5; +} + +.categoryContent { + min-height: 50px; +} + +.isOver { + background-color: #efefef; +} + +.isDragging { + opacity: 0.5; + z-index: 1000; +} + +.dragOverlay { + .sortableCategory, .productCard { + transform: scale(1.05); + box-shadow: 0 0 20px rgba(0, 0, 0, 0.2); + } +} + +.cards { + display: flex; + flex-direction: column; +} + +.productCard { + box-sizing: border-box; + border-radius: var(--tk-radius-sm); + border: 1px solid #e3e3e3; + background-color: #FFFFFF; + display: grid; + padding: 20px; + margin-bottom: 20px; + position: relative; + gap: 10px; + transition: transform 250ms ease; + grid-template-areas: "dragHanlde productInfo action"; + grid-template-columns: 40px 1fr 40px; + + @include respond-below(lg) { + grid-template-areas: "dragHanlde productInfo" + "dragHanlde action"; + } + + .halfCircle { + width: 20px; + height: 10px; + background-color: #fff; + border-top-left-radius: 110px; + border-top-right-radius: 110px; + border: 1px solid #ddd; + border-bottom: 0; + transform: rotate(90deg); + position: absolute; + left: -6px; + top: 44%; + } + + .halfCircle.right { + left: auto; + right: -6px; + transform: rotate(270deg); + } + + .dragHandle { + display: flex; + justify-content: center; + align-items: center; + cursor: move; + grid-area: dragHanlde; + touch-action: none; + } + + .dragHandleDisabled { + cursor: not-allowed; + opacity: 0.5; + } + + .productInfo { + grid-area: productInfo; + display: flex; + + .productDetails { + display: grid; + width: 100%; + align-items: center; + gap: 15px; + flex-wrap: wrap; + grid-template-columns: 1fr 1fr 1fr 1fr; + + @include respond-below(lg) { + flex-direction: column; + align-items: flex-start; + grid-template-columns: 1fr 1fr; + gap: 20px; + } + + @include respond-below(sm) { + gap: 10px; + } + + @include respond-below(xs) { + gap: 20px; + grid-template-columns: 1fr; + } + + > div { + flex: 1; + min-width: 125px; + + @include respond-below(sm) { + min-width: 100px; + } + } + + .heading { + text-transform: uppercase; + color: #9ca3af; + font-size: .8em; + } + + .status { + max-width: 120px; + cursor: pointer; + } + + .title { + text-wrap: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .price { + color: var(--tk-color-money-green); + + .priceAmount { + font-weight: 600; + text-wrap: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + .availability { + } + } + } + + .action { + display: flex; + grid-area: action; + + @include respond-below(lg) { + margin-top: 10px; + } + + .desktopAction { + @include respond-below(lg) { + display: none; + } + } + + .mobileAction { + display: none; + @include respond-below(lg) { + display: block; + } + } + } +} + +.dragPreview { + background-color: #fff; + border: 1px solid #e3e3e3; + border-radius: var(--tk-radius-sm); + padding: 10px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + + h3 { + margin: 0 0 5px; + } + + p { + margin: 0; + color: #666; + } +} + +.moreProducts { + background-color: #f0f0f0; + border-radius: var(--tk-radius-sm); + padding: 10px; + margin-top: 10px; + font-size: 14px; + color: #666; + text-align: center; +} + +.isDragging { + opacity: 0.6; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} diff --git a/frontend/src/components/common/ProductsTable/SortableCategory/index.tsx b/frontend/src/components/common/ProductsTable/SortableCategory/index.tsx new file mode 100644 index 00000000..186fa09c --- /dev/null +++ b/frontend/src/components/common/ProductsTable/SortableCategory/index.tsx @@ -0,0 +1,166 @@ +import React from 'react'; +import {IconEyeOff, IconPencil, IconPlus, IconTrash, IconTrashOff} from "@tabler/icons-react"; +import classes from "../ProductsTable.module.scss"; +import classNames from "classnames"; +import {ActionIcon, Popover} from "@mantine/core"; +import {useDisclosure} from "@mantine/hooks"; +import {EditProductCategoryModal} from "../../../modals/EditProductCategoryModal"; +import {ProductCategory} from "../../../../types.ts"; +import {t} from "@lingui/macro"; +import {useDeleteProductCategory} from "../../../../mutations/useDeleteProductCategory.ts"; +import {useParams} from "react-router-dom"; +import {showError, showSuccess} from "../../../../utilites/notifications.tsx"; +import {SortArrows} from "../../SortArrows"; +import {useSortProducts} from "../../../../mutations/useSortProducts.ts"; + +interface SortableCategoryProps { + category: ProductCategory; + children: React.ReactNode; + isLastCategory: boolean; + openCreateModal: () => void; + categories: ProductCategory[]; +} + +export const SortableCategory: React.FC = ({ + category, + children, + isLastCategory, + openCreateModal, + categories, + }) => { + const [isEditModalOpen, editModal] = useDisclosure(false); + const {eventId} = useParams(); + const deleteMutation = useDeleteProductCategory(); + const sortMutation = useSortProducts(); + const upSortEnabled = categories.findIndex(cat => cat.id === category.id) > 0; + const downSortEnabled = categories.findIndex(cat => cat.id === category.id) < categories.length - 1; + + const handleDelete = () => { + if (isLastCategory) { + showError(t`You cannot delete the last category.`); + return; + } + + deleteMutation.mutate({productCategoryId: category.id, eventId: eventId}, { + onSuccess: () => { + editModal.close(); + }, + onError: (error) => { + if (error?.response?.status && error.response.status === 409 && error?.response?.data?.message) { + showError(error?.response?.data.message); + return; + } else { + showError(t`We couldn't delete the category. Please try again.`); + } + } + }); + } + + const handleSort = (direction: 'up' | 'down') => { + if (!eventId || !category.id) return; + + const currentIndex = categories.findIndex(cat => cat.id === category.id); + + if (currentIndex === -1) return; + + const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1; + + if (targetIndex < 0 || targetIndex >= categories.length) return; + + const newCategories = [...categories]; + const [movedCategory] = newCategories.splice(currentIndex, 1); + newCategories.splice(targetIndex, 0, movedCategory); + + // Prepare the sorted categories data + const sortedCategories = newCategories.map(cat => + ({ + product_category_id: cat.id as number, + sorted_products: (cat.products || []).map((product, index) => ({ + id: product.id as number, + sort_order: index + })) + })); + + sortMutation.mutate( + { + eventId: eventId, + sortedCategories: sortedCategories + }, + { + onSuccess: () => { + showSuccess(t`Categories reordered successfully.`); + }, + onError: () => { + showError(t`We couldn't reorder the categories. Please try again.`); + } + } + ); + }; + + return ( + <> +
+
+

+ {category.name} + {category.is_hidden && ( + + + + + + {t`This category is hidden from public view`} + + + )} +

+ +
+ handleSort('up')} + onSortDown={() => handleSort('down')} + /> + + + + + + + + {isLastCategory ? : } + +
+
+
+ {children} +
+
+ {isEditModalOpen && ( + + )} + + ); +}; diff --git a/frontend/src/components/common/ProductsTable/SortableProduct/index.tsx b/frontend/src/components/common/ProductsTable/SortableProduct/index.tsx new file mode 100644 index 00000000..7a81be64 --- /dev/null +++ b/frontend/src/components/common/ProductsTable/SortableProduct/index.tsx @@ -0,0 +1,300 @@ +import {useState} from 'react'; +import {IconDotsVertical, IconEyeOff, IconPencil, IconSend, IconTrash} from "@tabler/icons-react"; +import classes from "../ProductsTable.module.scss"; +import classNames from "classnames"; +import {Badge, Button, Group, Menu, Popover} from "@mantine/core"; +import Truncate from "../../Truncate"; +import {t} from "@lingui/macro"; +import {relativeDate} from "../../../../utilites/dates.ts"; +import {formatCurrency} from "../../../../utilites/currency.ts"; +import { + IdParam, + MessageType, + Product, + ProductCategory, + ProductPrice, + ProductPriceType, + ProductType +} from "../../../../types.ts"; +import {useDisclosure} from "@mantine/hooks"; +import {useDeleteProduct} from "../../../../mutations/useDeleteProduct.ts"; +import {showError, showSuccess} from "../../../../utilites/notifications.tsx"; +import {EditProductModal} from "../../../modals/EditProductModal"; +import {SendMessageModal} from "../../../modals/SendMessageModal"; +import {SortArrows} from "../../SortArrows"; +import {useSortProducts} from "../../../../mutations/useSortProducts.ts"; + +interface SortableProductProps { + product: Product; + currencyCode: string; + category: ProductCategory; + categories: ProductCategory[]; +} + +export const SortableProduct = ({product, currencyCode, category, categories}: SortableProductProps) => { + const [isEditModalOpen, editModal] = useDisclosure(false); + const [isMessageModalOpen, messageModal] = useDisclosure(false); + const [productId, setProductId] = useState(); + const deleteMutation = useDeleteProduct(); + const sortMutation = useSortProducts(); + + if (!product?.id || !category?.id || !Array.isArray(category.products)) { + return null; + } + + const handleModalClick = (productId: IdParam, modal: { open: () => void }) => { + setProductId(productId); + modal.open(); + } + + const handleDeleteProduct = (productId: IdParam, eventId: IdParam) => { + deleteMutation.mutate({productId, eventId}, { + onSuccess: () => { + showSuccess(t`Product deleted successfully`); + }, + onError: (error: any) => { + if (error.response?.status === 409) { + showError(error.response.data.message || t`This product cannot be deleted because it is associated with an order. You can hide it instead.`); + } + } + }); + } + + const getProductStatus = (product: Product) => { + if (product.is_sold_out) { + return t`Sold Out`; + } + + if (product.is_before_sale_start_date) { + return t`On sale` + ' ' + relativeDate(product.sale_start_date as string); + } + + if (product.is_after_sale_end_date) { + return t`Sale ended ` + ' ' + relativeDate(product.sale_end_date as string); + } + + if (product.is_hidden) { + return t`Hidden from public view`; + } + + return product.is_available ? t`On Sale` : t`Not On Sale`; + } + + const getPriceRange = (product: Product) => { + const productPrices: ProductPrice[] = product.prices as ProductPrice[]; + if (!Array.isArray(productPrices) || productPrices.length === 0) { + return t`Price not set`; + } + + if (product.type !== ProductPriceType.Tiered) { + if (productPrices[0].price <= 0) { + return t`Free`; + } + return formatCurrency(productPrices[0].price, currencyCode); + } + + const prices = productPrices.map(productPrice => productPrice.price); + const minPrice = Math.min(...prices); + const maxPrice = Math.max(...prices); + + if (minPrice <= 0 && maxPrice <= 0) { + return t`Free`; + } + + return `${formatCurrency(minPrice, currencyCode)} - ${formatCurrency(maxPrice, currencyCode)}`; + } + + const handleSort = (productId: IdParam, direction: 'up' | 'down') => { + if (!category?.products?.length || !product.event_id) return; + + // Find current category index in all categories + const categoryIndex = categories.findIndex(cat => cat.id === category.id); + const currentIndex = category.products.findIndex(p => p.id === productId); + + if (categoryIndex === -1 || currentIndex === -1) return; + + let updatedCategories = [...categories]; + + // Handle moving to different category + if ((direction === 'up' && currentIndex === 0) || + (direction === 'down' && currentIndex === category.products.length - 1)) { + + const targetCategoryIndex = direction === 'up' ? categoryIndex - 1 : categoryIndex + 1; + + // Check if target category exists + if (targetCategoryIndex < 0 || targetCategoryIndex >= categories.length) return; + + const sourceProducts = [...category.products]; + const [movedProduct] = sourceProducts.splice(currentIndex, 1); + + const targetCategory = categories[targetCategoryIndex]; + const targetProducts = [...(targetCategory.products || [])]; + + // Insert at end if moving up, start if moving down + const targetPosition = direction === 'up' ? targetProducts.length : 0; + targetProducts.splice(targetPosition, 0, movedProduct); + + updatedCategories = categories.map((cat, index) => { + if (index === categoryIndex) { + return {...cat, products: sourceProducts}; + } + if (index === targetCategoryIndex) { + return {...cat, products: targetProducts}; + } + return cat; + }); + } else { + // Handle moving within same category + const newIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1; + if (newIndex < 0 || newIndex >= category.products.length) return; + + const updatedProducts = [...category.products]; + [updatedProducts[currentIndex], updatedProducts[newIndex]] = + [updatedProducts[newIndex], updatedProducts[currentIndex]]; + + updatedCategories = categories.map(cat => + cat.id === category.id ? {...cat, products: updatedProducts} : cat + ); + } + + const sortedCategories = updatedCategories.map(cat => ({ + product_category_id: cat.id, + sorted_products: (cat.products || []).map((prod, index) => ({ + id: prod.id, + order: index + 1 + })) + })); + + sortMutation.mutate({ + sortedCategories, + eventId: product.event_id, + }, { + onSuccess: () => showSuccess(t`Products sorted successfully`), + onError: () => showError(t`Failed to sort products`) + }); + }; + + const currentCategoryIndex = categories.findIndex(cat => cat.id === category.id); + const currentProducts = category.products || []; + const currentIndex = currentProducts.findIndex(p => p.id === product.id); + + const canMoveUp = currentIndex > 0 || currentCategoryIndex > 0; + const canMoveDown = currentIndex < currentProducts.length - 1 || + currentCategoryIndex < categories.length - 1; + + return ( + <> +
+
+ handleSort(product.id, 'up')} + onSortDown={() => handleSort(product.id, 'down')} + flexDirection={'column'} + /> +
+
+
+
+
{t`Title`} {product.id}
+ + {(product.is_hidden_without_promo_code || product.is_hidden) && ( + + + + + + {product.is_hidden + ? t`This product is hidden from public view` + : t`This product is hidden unless targeted by a Promo Code`} + + + )} +
+
+
{t`Status`}
+ + + + {product.is_available ? t`On Sale` : t`Not On Sale`} + + + + {getProductStatus(product)} + + +
+
+
{t`Price`}
+
+ {getPriceRange(product)} +
+
+
+
+ {product.product_type === ProductType.Ticket ? t`Attendees` : t`Quantity Sold`} +
+ {Number(product.quantity_sold)} +
+
+
+
+ + + +
+
+ +
+
+ +
+
+
+ + {t`Actions`} + + {product.product_type === ProductType.Ticket && ( + handleModalClick(product.id, messageModal)} + leftSection={}> + {t`Message Attendees`} + + )} + + handleModalClick(product.id, editModal)} + leftSection={}> + {t`Edit Product`} + + {t`Danger zone`} + handleDeleteProduct(product.id, product.event_id)} + color="red" + leftSection={}> + {t`Delete product`} + + +
+
+
+ {product.product_type === ProductType.Ticket &&
} +
+
+ {isEditModalOpen && } + {isMessageModalOpen && ( + + )} + + ); +}; diff --git a/frontend/src/components/common/ProductsTable/index.tsx b/frontend/src/components/common/ProductsTable/index.tsx new file mode 100644 index 00000000..597666f7 --- /dev/null +++ b/frontend/src/components/common/ProductsTable/index.tsx @@ -0,0 +1,109 @@ +import React, {useEffect, useState} from 'react'; +import {SortableProduct} from "./SortableProduct"; +import {SortableCategory} from "./SortableCategory"; +import classes from "./ProductsTable.module.scss"; +import {IdParam, Product, ProductCategory} from "../../../types.ts"; +import {ProductsBlankSlate} from "./ProductsBlankSlate"; + +export interface ProductCategoryListProps { + initialCategories: ProductCategory[]; + event: any; + onCreateOpen: (categoryId: IdParam) => void; + searchTerm: string; +} + +export const ProductCategoryList: React.FC = ({ + initialCategories, + event, + onCreateOpen, + searchTerm + }) => { + const [categories, setCategories] = useState(initialCategories); + const [filteredCategories, setFilteredCategories] = useState(initialCategories); + + useEffect(() => { + setCategories(initialCategories); + }, [initialCategories]); + + if (!categories || categories.length === 0 || !event) { + return <>no categories or event; + } + + useEffect(() => { + if (searchTerm) { + const lowercaseSearch = searchTerm.toLowerCase(); + const filtered = categories + .map(category => { + const categoryMatchesSearch = category.name.toLowerCase().includes(lowercaseSearch); + const filteredProducts = category.products?.filter(product => + product.title.toLowerCase().includes(lowercaseSearch) + ); + + return { + ...category, + products: categoryMatchesSearch + ? category.products + : filteredProducts + }; + }) + .filter(category => { + const hasMatchingProducts = category.products ? category.products.length > 0 : false; + const categoryNameMatches = category.name.toLowerCase().includes(lowercaseSearch); + return hasMatchingProducts || categoryNameMatches; + }); + + setFilteredCategories(filtered); + } else { + setFilteredCategories(categories); + } + }, [searchTerm, categories]); + + return ( +
+ {filteredCategories.length > 0 ? ( +
+ {filteredCategories.map((category) => { + if (!category?.products) return <>; + + return ( + onCreateOpen(category.id)} + isLastCategory={filteredCategories.length === 1} + categories={categories} + > + {category.products.length === 0 && ( + onCreateOpen(category.id)} + /> + )} + {category.products.length > 0 && ( +
+ {category.products.map((product: Product) => ( + + ))} +
+ )} +
+ ); + })} +
+ ) : ( + + )} +
+ ); +}; diff --git a/frontend/src/components/common/PromoCodeTable/index.tsx b/frontend/src/components/common/PromoCodeTable/index.tsx index 192b7a2f..6c24d597 100644 --- a/frontend/src/components/common/PromoCodeTable/index.tsx +++ b/frontend/src/components/common/PromoCodeTable/index.tsx @@ -25,6 +25,7 @@ export const PromoCodeTable = ({event, promoCodes, openCreateModal}: PromoCodeTa const [editModalOpen, {open: openEditModal, close: closeEditModal}] = useDisclosure(false); const deleteMutation = useDeletePromoCode(); const clipboard = useClipboard({ timeout: 500 }); + const eventProducts = event.product_categories?.flatMap(category => category.products); const handleEditModal = (promoCodeId: number | undefined) => { setPromoCodeId(promoCodeId); @@ -68,7 +69,7 @@ export const PromoCodeTable = ({event, promoCodes, openCreateModal}: PromoCodeTa {t`Code`} {t`Discount`} {t`Times used`} - {t`Tickets`} + {t`Products`} {t`Expires`} @@ -126,25 +127,25 @@ export const PromoCodeTable = ({event, promoCodes, openCreateModal}: PromoCodeTa
- {code.applicable_ticket_ids?.length === 0 && ( - {t`All Tickets`} + {code.applicable_product_ids?.length === 0 && ( + {t`All Products`} )} - {Number(code.applicable_ticket_ids?.length) > 0 && ( + {Number(code.applicable_product_ids?.length) > 0 && ( - code.applicable_ticket_ids?.map(Number)?.includes(Number(ticket.id))) - .map(ticket => { + eventProducts?.filter((product) => + code.applicable_product_ids?.map(Number)?.includes(Number(product.id))) + .map(product => { return ( <> - {ticket.title} + {product.title}
); })}> {code.applicable_ticket_ids?.length} {t`Ticket(s)`} + color={'pink'}>{code.applicable_product_ids?.length} {t`Product(s)`}
)}
diff --git a/frontend/src/components/common/QuestionAndAnswerList/QuestionAndAnswerList.module.scss b/frontend/src/components/common/QuestionAndAnswerList/QuestionAndAnswerList.module.scss new file mode 100644 index 00000000..ea556c5b --- /dev/null +++ b/frontend/src/components/common/QuestionAndAnswerList/QuestionAndAnswerList.module.scss @@ -0,0 +1,77 @@ +.container { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.section { + display: flex; + flex-direction: column; + gap: 5px; +} + +.sectionHeader { + display: flex; + align-items: center; + gap: 0.5rem; + padding-bottom: 0.75rem; +} + +.questionsList { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.questionCard { + padding: 1rem; + border-radius: var(--mantine-radius-sm); + background: var(--mantine-color-gray-0); + border: 1px solid var(--mantine-color-gray-2); + transition: all 0.2s ease; + + &:hover { + border-color: var(--mantine-color-gray-3); + } +} + +.productTitle { + color: var(--mantine-color-gray-6); + margin-bottom: 0.5rem; +} + +.questionTitle { + display: flex; + gap: 0.5rem; + align-items: flex-start; + margin-bottom: 0.75rem; + + svg { + margin-top: 0; + } +} + +.answer { + color: var(--mantine-color-dark-9); + line-height: 1.5; + padding: 0.5rem 0; +} + +.attendeeInfo { + display: flex; + align-items: center; + gap: 0.5rem; + margin-top: 0.75rem; + padding-top: 0.75rem; + border-top: 1px solid var(--mantine-color-gray-2); +} + +.emptyState { + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; + background: var(--mantine-color-gray-0); + border-radius: var(--mantine-radius-sm); + color: var(--mantine-color-gray-6); +} diff --git a/frontend/src/components/common/QuestionAndAnswerList/index.tsx b/frontend/src/components/common/QuestionAndAnswerList/index.tsx index c92dabc4..29a8bfc6 100644 --- a/frontend/src/components/common/QuestionAndAnswerList/index.tsx +++ b/frontend/src/components/common/QuestionAndAnswerList/index.tsx @@ -1,21 +1,120 @@ -import {Card} from "../Card"; import {QuestionAnswer} from "../../../types.ts"; +import {ActionIcon, Group, Text, Tooltip} from '@mantine/core'; +import {t} from "@lingui/macro"; +import {NavLink} from "react-router-dom"; +import {IconExternalLink, IconMessageCircle2, IconPackage, IconShoppingCart, IconUser} from "@tabler/icons-react"; +import classes from './QuestionAndAnswerList.module.scss'; interface QuestionAndAnswerListProps { - questionAnswers: QuestionAnswer[] + questionAnswers: QuestionAnswer[]; + belongsToFilter?: string[]; } -export const QuestionAndAnswerList = ({questionAnswers}: QuestionAndAnswerListProps) => { +export const QuestionAndAnswerList = ({questionAnswers, belongsToFilter}: QuestionAndAnswerListProps) => { + const filteredQuestions = belongsToFilter?.length + ? questionAnswers.filter(qa => belongsToFilter.includes(qa.belongs_to)) + : questionAnswers; + + const productQuestions = filteredQuestions.filter(qa => qa.belongs_to === 'PRODUCT' && !qa.attendee_id); + const attendeeQuestions = filteredQuestions.filter(qa => qa.belongs_to === 'PRODUCT' && qa.attendee_id); + const orderQuestions = filteredQuestions.filter(qa => qa.belongs_to === 'ORDER'); + + const renderSection = (title: string, questions: QuestionAnswer[]) => { + const getIcon = () => { + switch (title) { + case 'Attendee Answers': + return ; + case 'Order Answers': + return ; + case 'Product Answers': + return ; + default: + return null; + } + }; + + return ( +
+ + + {getIcon()} + {title} + + + {questions.length} {questions.length === 1 ? 'response' : 'responses'} + + + + {questions.length > 0 ? ( +
+ {questions.map((qa, index) => ( +
+ {qa.product_title && ( + + {qa.product_title} + + )} + +
+ + + {qa.title} + +
+ + + {/*{Array.isArray(qa.answer) ? qa.answer.join(", ") : qa.answer}*/} + ddd + + + {qa.attendee_id && ( +
+ + + {qa.first_name + ? `${qa.first_name} ${qa.last_name}` + : t`N/A`} + + + + + + + + +
+ )} +
+ ))} +
+ ) : ( +
+ + {t`No ${title.toLowerCase()} available.`} + +
+ )} +
+ ); + }; + return ( - - {questionAnswers.map((answer, index) => ( -
- {answer.title} -

- {answer.text_answer} -

-
- ))} -
+
+ {orderQuestions.length > 0 && renderSection('Order Answers', orderQuestions)} + {attendeeQuestions.length > 0 && renderSection('Attendee Answers', attendeeQuestions)} + {productQuestions.length > 0 && renderSection('Product Answers', productQuestions)} +
); -} \ No newline at end of file +}; diff --git a/frontend/src/components/common/QuestionsTable/index.tsx b/frontend/src/components/common/QuestionsTable/index.tsx index bf4835e3..cfa0f8ce 100644 --- a/frontend/src/components/common/QuestionsTable/index.tsx +++ b/frontend/src/components/common/QuestionsTable/index.tsx @@ -222,7 +222,7 @@ const DefaultQuestions = () => ( ); export const QuestionsTable = ({questions}: QuestionsTableProp) => { - const ticketQuestions = questions.filter(question => question.belongs_to === "TICKET"); + const productQuestions = questions.filter(question => question.belongs_to === "PRODUCT"); const orderQuestions = questions.filter(question => question.belongs_to === "ORDER"); const form = useForm(); const [createModalOpen, {open: openCreateModal, close: closeCreateModal}] = useDisclosure(false); @@ -304,13 +304,13 @@ export const QuestionsTable = ({questions}: QuestionsTableProp) => { )}
-

{t`Attendee questions`}

+

{t`Product questions`}

- {ticketQuestions + {productQuestions .filter(question => showHiddenQuestions || !question.is_hidden) .length === 0 && ( @@ -347,7 +347,7 @@ export const QuestionsTable = ({questions}: QuestionsTableProp) => {

{t`Attendee questions`}

- {ticketQuestions + {productQuestions .filter(question => showHiddenQuestions || !question.is_hidden) .map(question => ( { + key: keyof T; + label: string; + render?: (value: any, row: T) => React.ReactNode; + sortable?: boolean; +} + +interface ReportProps { + title: string; + columns: Column[]; + event: Event + isLoading?: boolean; + showDateFilter?: boolean; + defaultStartDate?: Date; + defaultEndDate?: Date; + onDateRangeChange?: (range: [Date | null, Date | null]) => void; + enableDownload?: boolean; + downloadFileName?: string; + showCustomDatePicker?: boolean; +} + +const TIME_PERIODS = [ + {value: '24h', label: t`Last 24 hours`}, + {value: '48h', label: t`Last 48 hours`}, + {value: '7d', label: t`Last 7 days`}, + {value: '14d', label: t`Last 14 days`}, + {value: '30d', label: t`Last 30 days`}, + {value: '90d', label: t`Last 90 days`}, + {value: '6m', label: t`Last 6 months`}, + {value: 'ytd', label: t`Year to date`}, + {value: '12m', label: t`Last 12 months`}, + {value: 'custom', label: t`Custom Range`} +]; + +const ReportTable = >({ + title, + columns, + showDateFilter = true, + defaultStartDate = new Date(new Date().setMonth(new Date().getMonth() - 3)), + defaultEndDate = new Date(), + onDateRangeChange, + enableDownload = true, + downloadFileName = 'report.csv', + showCustomDatePicker = false, + event + }: ReportProps) => { + const [dateRange, setDateRange] = useState<[Date | null, Date | null]>([ + dayjs(defaultStartDate).tz(event.timezone).toDate(), + dayjs(defaultEndDate).tz(event.timezone).toDate() + ]); + const [selectedPeriod, setSelectedPeriod] = useState('90d'); + const [showDatePickerInput, setShowDatePickerInput] = useState(showCustomDatePicker); + const [sortField, setSortField] = useState(null); + const [sortDirection, setSortDirection] = useState<'asc' | 'desc' | null>(null); + const {reportType, eventId} = useParams(); + const reportQuery = useGetEventReport(eventId, reportType, dateRange[0], dateRange[1]); + const data = (reportQuery.data || []) as T[]; + + const calculateDateRange = (period: string): [Date | null, Date | null] => { + if (period === 'custom') { + setShowDatePickerInput(true); + return dateRange; + } + setShowDatePickerInput(false); + + let end = dayjs().tz(event.timezone).endOf('day'); + let start = dayjs().tz(event.timezone); + + switch (period) { + case '24h': + start = start.startOf('day'); + end = start.endOf('day'); + break; + case '48h': + start = start.subtract(1, 'day').startOf('day'); + end = start.endOf('day').add(1, 'day'); + break; + case '7d': + start = start.subtract(6, 'day').startOf('day'); + break; + case '14d': + start = start.subtract(13, 'day').startOf('day'); + break; + case '30d': + start = start.subtract(29, 'day').startOf('day'); + break; + case '90d': + start = start.subtract(89, 'day').startOf('day'); + break; + case '6m': + start = start.subtract(6, 'month').startOf('day'); + break; + case 'ytd': + start = start.startOf('year'); + break; + case '12m': + start = start.subtract(12, 'month').startOf('day'); + break; + default: + return [null, null]; + } + + return [start.toDate(), end.toDate()]; + }; + + const handlePeriodChange = (value: string | null, _: ComboboxItem) => { + if (!value) return; + setSelectedPeriod(value); + const newRange = calculateDateRange(value); + setDateRange(newRange); + onDateRangeChange?.(newRange); + }; + + const handleDateRangeChange = (newRange: [Date | null, Date | null]) => { + const [start, end] = newRange; + const tzStart = start ? dayjs(start).tz(event.timezone) : null; + const tzEnd = end ? dayjs(end).tz(event.timezone) : null; + + const tzRange: [Date | null, Date | null] = [ + tzStart?.toDate() || null, + tzEnd?.toDate() || null + ]; + + setDateRange(tzRange); + onDateRangeChange?.(tzRange); + }; + + const handleSort = (field: keyof T) => { + if (sortField === field) { + if (sortDirection === 'asc') setSortDirection('desc'); + else if (sortDirection === 'desc') { + setSortDirection(null); + setSortField(null); + } else setSortDirection('asc'); + } else { + setSortField(field); + setSortDirection('asc'); + } + }; + + const getSortIcon = (field: keyof T) => { + if (sortField !== field) return ; + if (sortDirection === 'asc') return ; + if (sortDirection === 'desc') return ; + return ; + }; + + const sortedData = useMemo(() => { + return [...data].sort((a, b) => { + if (!sortField || !sortDirection) return 0; + const aValue = a[sortField]; + const bValue = b[sortField]; + + const aNum = Number(aValue); + const bNum = Number(bValue); + + if (!isNaN(aNum) && !isNaN(bNum)) { + return sortDirection === 'asc' ? aNum - bNum : bNum - aNum; + } + + if (typeof aValue === 'string' && typeof bValue === 'string') { + return sortDirection === 'asc' + ? aValue.toLowerCase().localeCompare(bValue.toLowerCase()) + : bValue.toLowerCase().localeCompare(aValue.toLowerCase()); + } + + return 0; + }); + }, [data, sortField, sortDirection]); + + const csvHeaders = columns.map(col => col.label); + const csvData = sortedData.map(row => + columns.map(col => { + const value = row[col.key]; + return typeof value === 'number' ? value.toString() : value; + }) + ); + + const loadingMessage = () => { + const wrapper = (message: React.ReactNode) => ( + + + {message} + + + ); + + if (reportQuery.isLoading) { + return wrapper(t`Loading...`); + } + + if (showDateFilter && (!dateRange[0] || !dateRange[1])) { + return wrapper(t`No data to show. Please select a date range`); + } + + if (!showDateFilter && dateRange[0] && dateRange[1]) { + return wrapper(t`No data available`); + } + }; + + if (reportQuery.isLoading) { + return ( + <> + + + + + + + + + + ); + } + + if (reportQuery.isFetched && !reportQuery.isLoading && !data.length) { + return ( + +

+ {t`Once you start collecting data, you'll see it here.`} +

+ + )} + /> + ); + } + + return ( + <> + + {title} + + {showDateFilter && ( + - {({ copied, copy }) => ( + {({copied, copy}) => ( {copied ? : } diff --git a/frontend/src/components/common/SortArrows/index.tsx b/frontend/src/components/common/SortArrows/index.tsx new file mode 100644 index 00000000..fe0dbdce --- /dev/null +++ b/frontend/src/components/common/SortArrows/index.tsx @@ -0,0 +1,43 @@ +import {ActionIcon} from '@mantine/core'; +import {IconCaretDownFilled, IconCaretUpFilled} from '@tabler/icons-react'; + +interface SortArrowsProps { + upArrowEnabled: boolean; + downArrowEnabled: boolean; + onSortUp: () => void; + onSortDown: () => void; + flexDirection?: 'row' | 'column'; +} + +export const SortArrows = ({ + upArrowEnabled, + downArrowEnabled, + onSortUp, + onSortDown, + flexDirection = 'row' + }: SortArrowsProps) => { + return ( +
+ + + + + + +
+ ); +}; diff --git a/frontend/src/components/common/StatBoxes/StatBoxes.module.scss b/frontend/src/components/common/StatBoxes/StatBoxes.module.scss index 7abeb018..c6aff0b0 100644 --- a/frontend/src/components/common/StatBoxes/StatBoxes.module.scss +++ b/frontend/src/components/common/StatBoxes/StatBoxes.module.scss @@ -1,57 +1,100 @@ +@import "../../../styles/mixins.scss"; + .statistics { - display: flex; - justify-content: space-around; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; color: var(--tk-primary); - gap: 15px; + margin: 0.5rem 0; + + @include respond-below(lg) { + grid-template-columns: repeat(2, 1fr); + } + + @include respond-below(md) { + grid-template-columns: 1fr; + } } .statistic { - flex: 1 1 50px; display: flex; - flex-direction: row; - padding: 20px; - min-width: 250px; - margin-bottom: 0; + align-items: center; + padding: 1.25rem !important; + transition: all 0.2s ease-in-out; + margin-bottom: 0 !important; + height: 100%; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + } .leftPanel { - display: flex; - flex-direction: column; - gap: 10px; + flex: 1; + display: grid; + grid-template-rows: auto auto; + min-height: 65px; + gap: 5px; .number { - font-size: 1.5em; - font-weight: bold; + font-size: 1.75rem; + font-weight: 600; + letter-spacing: -0.02em; + line-height: 1.2; + align-self: end; + + @include respond-below(md) { + font-size: 1.5rem; + } } .description { - font-size: 1em; + font-size: 1rem; color: var(--tk-color-gray-dark); - } - - .change { - font-size: 1em; + font-weight: 500; + align-self: start; } } .rightPanel { - flex: 1; - display: flex; - place-content: flex-end; - align-items: center; - + margin-left: 1rem; + align-self: flex-start; .icon { - width: 40px; - height: 40px; - background: #63d57e; - border-radius: 50px; + width: 42px; + height: 42px; + border-radius: 12px; display: flex; align-items: center; justify-content: center; color: #ffffff; + transition: transform 0.2s ease; + + @include respond-below(md) { + width: 36px; + height: 36px; + border-radius: 10px; + } + + &:hover { + transform: scale(1.05); + } } } -} + @include respond-below(sm) { + padding: 1rem !important; + .leftPanel { + min-height: 55px; + + .number { + font-size: 1.25rem; + } + + .description { + font-size: 0.9125rem; + } + } + } +} diff --git a/frontend/src/components/common/StatBoxes/index.tsx b/frontend/src/components/common/StatBoxes/index.tsx index 958ac5c5..dd4746dc 100644 --- a/frontend/src/components/common/StatBoxes/index.tsx +++ b/frontend/src/components/common/StatBoxes/index.tsx @@ -1,5 +1,5 @@ import classes from "./StatBoxes.module.scss"; -import {IconCash, IconEye, IconReceipt, IconTicket} from "@tabler/icons-react"; +import {IconCash, IconCreditCardRefund, IconEye, IconReceipt, IconShoppingCart, IconUsers} from "@tabler/icons-react"; import {Card} from "../Card"; import {useGetEventStats} from "../../../queries/useGetEventStats.ts"; import {useParams} from "react-router-dom"; @@ -17,24 +17,40 @@ export const StatBoxes = () => { const data = [ { - number: formatNumber(eventStats?.total_tickets_sold as number), - description: t`Tickets sold`, - icon: + number: formatNumber(eventStats?.total_products_sold as number), + description: t`Products sold`, + icon: , + backgroundColor: '#4B7BE5' // Deep blue + }, + { + number: formatNumber(eventStats?.total_attendees_registered as number), + description: t`Attendees`, + icon: , + backgroundColor: '#E6677E' // Rose pink + }, + { + number: formatCurrency(eventStats?.total_refunded as number || 0, event?.currency), + description: t`Refunded`, + icon: , + backgroundColor: '#49A6B7' // Teal }, { number: formatCurrency(eventStats?.total_gross_sales || 0, event?.currency), description: t`Gross sales`, - icon: + icon: , + backgroundColor: '#7C63E6' // Purple }, { number: formatNumber(eventStats?.total_views as number), description: t`Page views`, - icon: + icon: , + backgroundColor: '#63B3A1' // Sage green }, { number: formatNumber(eventStats?.total_orders as number), description: t`Orders Created`, - icon: + icon: , + backgroundColor: '#E67D49' // Coral orange } ]; @@ -46,7 +62,7 @@ export const StatBoxes = () => {
{stat.description}
-
+
{stat.icon}
@@ -60,4 +76,3 @@ export const StatBoxes = () => {
); }; - diff --git a/frontend/src/components/common/TicketsTable/SortableTicket/index.tsx b/frontend/src/components/common/TicketsTable/SortableTicket/index.tsx deleted file mode 100644 index 0807b7bf..00000000 --- a/frontend/src/components/common/TicketsTable/SortableTicket/index.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import {IdParam, MessageType, Ticket, TicketPrice, TicketType} from "../../../../types.ts"; -import {useSortable} from "@dnd-kit/sortable"; -import {useDisclosure} from "@mantine/hooks"; -import {useState} from "react"; -import {useDeleteTicket} from "../../../../mutations/useDeleteTicket.ts"; -import {CSS} from "@dnd-kit/utilities"; -import {showError, showSuccess} from "../../../../utilites/notifications.tsx"; -import {t} from "@lingui/macro"; -import {relativeDate} from "../../../../utilites/dates.ts"; -import {formatCurrency} from "../../../../utilites/currency.ts"; -import {Card} from "../../Card"; -import classes from "../TicketsTable.module.scss"; -import classNames from "classnames"; -import {IconDotsVertical, IconEyeOff, IconGripVertical, IconPencil, IconSend, IconTrash} from "@tabler/icons-react"; -import Truncate from "../../Truncate"; -import {Badge, Button, Group, Menu, Popover} from "@mantine/core"; -import {EditTicketModal} from "../../../modals/EditTicketModal"; -import {SendMessageModal} from "../../../modals/SendMessageModal"; -import {UniqueIdentifier} from "@dnd-kit/core"; - -export const SortableTicket = ({ticket, enableSorting, currencyCode}: {ticket: Ticket, enableSorting: boolean, currencyCode: string }) => { - const uniqueId = ticket.id as UniqueIdentifier; - const { - attributes, - listeners, - setNodeRef, - transform, - transition - } = useSortable( - { - id: uniqueId, - } - ); - const [isEditModalOpen, editModal] = useDisclosure(false); - const [isMessageModalOpen, messageModal] = useDisclosure(false); - const [ticketId, setTicketId] = useState(); - const deleteMutation = useDeleteTicket(); - - const style = { - transform: CSS.Transform.toString(transform), - transition, - }; - - const handleModalClick = (ticketId: IdParam, modal: { open: () => void }) => { - setTicketId(ticketId); - modal.open(); - } - - const handleDeleteTicket = (ticketId: IdParam, eventId: IdParam) => { - deleteMutation.mutate({ticketId, eventId}, { - onSuccess: () => { - showSuccess(t`Ticket deleted successfully`) - }, - onError: (error: any) => { - if (error.response?.status === 409) { - showError(error.response.data.message || t`This ticket cannot be deleted because it is - associated with an order. You can hide it instead.`); - } - } - }); - } - - const getTicketStatus = (ticket: Ticket) => { - if (ticket.is_sold_out) { - return t`Sold Out`; - } - - if (ticket.is_before_sale_start_date) { - return t`On sale` + ' ' + relativeDate(ticket.sale_start_date as string); - } - - if (ticket.is_after_sale_end_date) { - return t`Sale ended ` + ' ' + relativeDate(ticket.sale_end_date as string); - } - - if (ticket.is_hidden) { - return t`Hidden from public view`; - } - - return ticket.is_available ? t`On Sale` : t`Not On Sale`; - } - - const getPriceRange = (ticket: Ticket) => { - const ticketPrices: TicketPrice[] = ticket.prices as TicketPrice[]; - - if (ticket.type !== TicketType.Tiered) { - if (ticketPrices[0].price <= 0) { - return t`Free`; - } - return formatCurrency(ticketPrices[0].price, currencyCode); - } - - if (ticketPrices.length === 0) { - return formatCurrency(ticketPrices[0].price, currencyCode) - } - - const prices = ticketPrices.map(ticketPrice => ticketPrice.price); - const minPrice = Math.min(...prices); - const maxPrice = Math.max(...prices); - - if (minPrice <= 0 && maxPrice <= 0) { - return t`Free`; - } - - return formatCurrency(minPrice, currencyCode) + ' - ' + formatCurrency(maxPrice, currencyCode); - } - - return ( - <> -
- -
- -
-
-
-
-
{t`Title`}
- {(ticket.is_hidden_without_promo_code || ticket.is_hidden) && ( - - - - - - {ticket.is_hidden - ? t`This ticket is hidden from public view` - : t`This ticket is hidden unless targeted by a Promo Code`} - - - )} -
-
-
{t`Status`}
- - - - {ticket.is_available ? t`On Sale` : t`Not On Sale`} - - - - {getTicketStatus(ticket)} - - - -
-
-
{t`Price`}
-
- {getPriceRange(ticket)} -
-
-
-
{t`Attendees`}
- {Number(ticket.quantity_sold)} -
-
-
-
- - - -
-
- -
-
- -
-
-
- - - {t`Actions`} - handleModalClick(ticket.id, messageModal)} - leftSection={}>{t`Message Attendees`} - handleModalClick(ticket.id, editModal)} - leftSection={}>{t`Edit Ticket`} - - {t`Danger zone`} - handleDeleteTicket(ticket.id, ticket.event_id)} - color="red" - leftSection={} - > - {t`Delete ticket`} - - -
-
-
-
-
- -
- - {isEditModalOpen && } - {isMessageModalOpen && } - - ); -}; diff --git a/frontend/src/components/common/TicketsTable/TicketsTable.module.scss b/frontend/src/components/common/TicketsTable/TicketsTable.module.scss deleted file mode 100644 index 39e9dc0b..00000000 --- a/frontend/src/components/common/TicketsTable/TicketsTable.module.scss +++ /dev/null @@ -1,152 +0,0 @@ -@import "../../../styles/mixins.scss"; - -.cards { - display: flex; - flex-direction: column; - - .ticketCard { - display: grid; - padding: 20px; - margin-bottom: 20px; - //border-top: 3px solid var(--tk-color-money-green) !important; - position: relative; - gap: 10px; - - grid-template-areas: "dragHanlde ticketInfo action"; - grid-template-columns: 40px 1fr 40px; - - @include respond-below(lg) { - grid-template-areas: "dragHanlde ticketInfo" - "dragHanlde action"; - } - - .halfCircle { - width: 20px; - height: 10px; - background-color: #fbfafb; - border-top-left-radius: 110px; - border-top-right-radius: 110px; - border: 1px solid #ddd; - border-bottom: 0; - transform: rotate(90deg); - position: absolute; - left: -6px; - top: 44%; - } - - .halfCircle.right { - left: auto; - right: -6px; - transform: rotate(270deg); - } - - .dragHandle { - display: flex; - justify-content: center; - align-items: center; - cursor: move; - grid-area: dragHanlde; - touch-action: none; - } - - .dragHandleDisabled { - cursor: not-allowed; - opacity: 0.5; - } - - .ticketInfo { - grid-area: ticketInfo; - - .ticketDetails { - display: grid; - width: 100%; - align-items: center; - gap: 15px; - flex-wrap: wrap; - - grid-template-columns: 1fr 1fr 1fr 1fr; - - @include respond-below(lg) { - flex-direction: column; - align-items: flex-start; - grid-template-columns: 1fr 1fr; - gap: 20px; - } - - @include respond-below(sm) { - gap: 10px; - } - - @include respond-below(xs) { - gap: 20px; - grid-template-columns: 1fr; - } - - > div { - flex: 1; - min-width: 125px; - - @include respond-below(sm) { - min-width: 100px; - } - } - - .heading { - text-transform: uppercase; - color: #9ca3af; - font-size: .8em; - } - - .status { - max-width: 120px; - cursor: pointer; - } - - .title { - text-wrap: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .price { - color: var(--tk-color-money-green); - - .priceAmount { - font-weight: 600; - text-wrap: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - } - - .availability { - } - } - } - - .action { - display: flex; - grid-area: action; - - @include respond-below(lg) { - margin-top: 10px; - } - - .desktopAction { - @include respond-below(lg) { - display: none; - } - } - - .mobileAction { - display: none; - @include respond-below(lg) { - display: block; - } - } - } - } -} - - - diff --git a/frontend/src/components/common/TicketsTable/index.tsx b/frontend/src/components/common/TicketsTable/index.tsx deleted file mode 100644 index 15eb8f4e..00000000 --- a/frontend/src/components/common/TicketsTable/index.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import {useEffect} from 'react'; -import classes from './TicketsTable.module.scss'; -import {NoResultsSplash} from "../NoResultsSplash"; -import {t} from "@lingui/macro"; -import { - closestCenter, - DndContext, - PointerSensor, - TouchSensor, - UniqueIdentifier, - useSensor, - useSensors, -} from '@dnd-kit/core'; -import {SortableContext, verticalListSortingStrategy,} from '@dnd-kit/sortable'; -import {Ticket, Event} from "../../../types"; -import {useSortTickets} from "../../../mutations/useSortTickets.ts"; -import {useParams} from "react-router-dom"; -import {showError, showSuccess} from "../../../utilites/notifications.tsx"; -import {SortableTicket} from "./SortableTicket"; -import {useDragItemsHandler} from "../../../hooks/useDragItemsHandler.ts"; -import {Button} from "@mantine/core"; -import {IconPlus} from "@tabler/icons-react"; - -interface TicketCardProps { - tickets: Ticket[]; - event: Event; - enableSorting: boolean; - openCreateModal: () => void; -} - -export const TicketsTable = ({tickets, event, openCreateModal, enableSorting = false}: TicketCardProps) => { - const {eventId} = useParams(); - const sortTicketsMutation = useSortTickets(); - const {items, setItems, handleDragEnd} = useDragItemsHandler({ - initialItemIds: tickets.map((ticket) => Number(ticket.id)), - onSortEnd: (newArray) => { - sortTicketsMutation.mutate({ - sortedTickets: newArray.map((id, index) => { - return {id, order: index + 1}; - }), - eventId: eventId, - }, { - onSuccess: () => { - showSuccess(t`Tickets sorted successfully`); - }, - onError: () => { - showError(t`An error occurred while sorting the tickets. Please try again or refresh the page`); - } - }) - }, - }); - - const sensors = useSensors( - useSensor(PointerSensor), - useSensor(TouchSensor) - ); - - useEffect(() => { - setItems(tickets.map((ticket) => Number(ticket.id))); - }, [tickets]); - - if (tickets.length === 0) { - return -

- {t`You'll need at least one ticket to get started. Free, paid or let the user decide what to pay.`} -

- - - )} - />; - } - - const handleDragStart = (event: any) => { - if (!enableSorting) { - showError(t`Please remove filters and set sorting to "Homepage order" to enable sorting`); - event.cancel(); - } - } - - return ( - - -
- {items.map((ticketId) => { - const ticket = tickets.find((t) => t.id === ticketId); - - if (!ticket) { - return null; - } - - return ( - - ); - })} -
-
-
- ); -}; diff --git a/frontend/src/components/common/ToolBar/index.tsx b/frontend/src/components/common/ToolBar/index.tsx index 60ab4465..f8c5a61c 100644 --- a/frontend/src/components/common/ToolBar/index.tsx +++ b/frontend/src/components/common/ToolBar/index.tsx @@ -11,13 +11,13 @@ export const ToolBar = ({searchComponent, children}: ToolBarProps) => { return (
-
+ {searchComponent &&
{searchComponent && searchComponent()} -
+
}
{children}
) -} \ No newline at end of file +} diff --git a/frontend/src/components/common/WidgetEditor/index.tsx b/frontend/src/components/common/WidgetEditor/index.tsx index 93a81261..0ae959a1 100644 --- a/frontend/src/components/common/WidgetEditor/index.tsx +++ b/frontend/src/components/common/WidgetEditor/index.tsx @@ -1,5 +1,5 @@ import classes from './WidgetEditor.module.scss'; -import SelectTickets from "../../routes/ticket-widget/SelectTickets"; +import SelectProducts from "../../routes/product-widget/SelectProducts"; import {ColorInput, Group, NumberInput, Switch, Tabs, Textarea, TextInput} from "@mantine/core"; import {t, Trans} from "@lingui/macro"; import {matches, useForm} from "@mantine/form"; @@ -308,7 +308,7 @@ export default App;

- {t`Ticket Widget Preview`} + {t`Product Widget Preview`}

@@ -332,7 +332,7 @@ export default App;
{!eventQuery.isFetched ? : - , +} + +// TODO: translations +export const ApiKeyForm = ({form}: ApiKeyFormProps) => { + return ( + <> + + + General" + }, + { + value: "events-products", + label: "Events -> Products" + }, + { + value: "events-stats", + label: "Events -> Stats" + }, + { + value: "events-attendees", + label: "Events -> Attendees" + }, + { + value: "events-orders", + label: "Events -> Orders" + }, + { + value: "events-questions", + label: "Events -> Questions" + }, + { + value: "events-images", + label: "Events -> Images" + }, + { + value: "events-promo-codes", + label: "Events -> Promo Codes" + }, + { + value: "events-messages", + label: "Events -> Messages" + }, + { + value: "events-settings", + label: "Events -> Settings" + }, + { + value: "events-capacity-assignments", + label: "Events -> Capacity Assignments" + }, + { + value: "events-check-in-lists", + label: "Events -> Check-In Lists" + }, + { + value: "events-reports", + label: "Events -> Reports" + }, + ]} + {...form.getInputProps('abilities')} + /> + + + + ); +}; diff --git a/frontend/src/components/forms/CapaciyAssigmentForm/index.tsx b/frontend/src/components/forms/CapaciyAssigmentForm/index.tsx index 6645dbf5..cfbbea6a 100644 --- a/frontend/src/components/forms/CapaciyAssigmentForm/index.tsx +++ b/frontend/src/components/forms/CapaciyAssigmentForm/index.tsx @@ -1,23 +1,24 @@ import {InputGroup} from "../../common/InputGroup"; -import {MultiSelect, NumberInput, TextInput} from "@mantine/core"; +import {NumberInput, TextInput} from "@mantine/core"; import {t} from "@lingui/macro"; import {UseFormReturnType} from "@mantine/form"; -import {CapacityAssignmentRequest, Ticket} from "../../../types.ts"; +import {CapacityAssignmentRequest, ProductCategory} from "../../../types.ts"; import {CustomSelect, ItemProps} from "../../common/CustomSelect"; -import {IconCheck, IconTicket, IconX} from "@tabler/icons-react"; +import {IconCheck, IconX} from "@tabler/icons-react"; +import {ProductSelector} from "../../common/ProductSelector"; interface CapacityAssigmentFormProps { form: UseFormReturnType; - tickets: Ticket[], + productsCategories: ProductCategory[], } -export const CapacityAssigmentForm = ({form, tickets}: CapacityAssigmentFormProps) => { +export const CapacityAssigmentForm = ({form, productsCategories}: CapacityAssigmentFormProps) => { const statusOptions: ItemProps[] = [ { icon: , label: t`Active`, value: 'ACTIVE', - description: t`Enable this capacity to stop ticket sales when the limit is reached`, + description: t`Enable this capacity to stop product sales when the limit is reached`, }, { icon: , @@ -43,18 +44,12 @@ export const CapacityAssigmentForm = ({form, tickets}: CapacityAssigmentFormProp /> - { - return { - value: String(ticket.id), - label: ticket.title, - } - })} - leftSection={} - {...form.getInputProps('ticket_ids')} + ; - tickets: Ticket[], + productCategories: ProductCategory[], } -export const CheckInListForm = ({form, tickets}: CheckInListFormProps) => { +export const CheckInListForm = ({form, productCategories}: CheckInListFormProps) => { return ( <> { placeholder={t`VIP check-in list`} /> - { - return { - value: String(ticket.id), - label: ticket.title, - } - })} - required - leftSection={} - {...form.getInputProps('ticket_ids')} + productCategories={productCategories} + form={form} + productFieldName="product_ids" + includedProductTypes={[ProductType.Ticket]} />