diff --git a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitOrdersApiController.php b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitOrdersApiController.php index 776f8e342..3bee659c5 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitOrdersApiController.php +++ b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitOrdersApiController.php @@ -1183,4 +1183,20 @@ public function deActivateTicket($summit_id, $order_id, $ticket_id) )); }); } + + /** + * @param $summit_id + * @param $order_id + * @return \Illuminate\Http\JsonResponse|mixed + */ + public function getOrderConfirmationEmailPDF($summit_id, $order_id) + { + return $this->processRequest(function () use ($summit_id, $order_id) { + $summit = SummitFinderStrategyFactory::build($this->summit_repository, $this->getResourceServerContext())->find($summit_id); + if (is_null($summit)) return $this->error404(); + + $content = $this->service->renderOrderConfirmationEmail($summit, intval($order_id)); + return $this->pdf("order_{$order_id}_confirmation_email.pdf", $content); + }); + } } \ No newline at end of file diff --git a/app/Http/Renderers/HTML2PDFRenderer.php b/app/Http/Renderers/HTML2PDFRenderer.php new file mode 100644 index 000000000..a42e5742f --- /dev/null +++ b/app/Http/Renderers/HTML2PDFRenderer.php @@ -0,0 +1,78 @@ +html = $html; + } + + public function render(): string + { + // create new PDF document + $pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false); + + // set document information + $pdf->SetCreator(PDF_CREATOR); + $pdf->SetTitle(''); + + // remove default header/footer + $pdf->setPrintHeader(false); + $pdf->setPrintFooter(false); + + // set header and footer fonts + $pdf->setHeaderFont(Array(PDF_FONT_NAME_MAIN, '', PDF_FONT_SIZE_MAIN)); + $pdf->setFooterFont(Array(PDF_FONT_NAME_DATA, '', PDF_FONT_SIZE_DATA)); + + // set default monospaced font + $pdf->SetDefaultMonospacedFont(PDF_FONT_MONOSPACED); + + // set margins + $pdf->SetMargins(PDF_MARGIN_LEFT, PDF_MARGIN_TOP, PDF_MARGIN_RIGHT); + $pdf->SetHeaderMargin(PDF_MARGIN_HEADER); + $pdf->SetFooterMargin(PDF_MARGIN_FOOTER); + + // set auto page breaks + $pdf->SetAutoPageBreak(TRUE, PDF_MARGIN_BOTTOM); + + // set image scale factor + $pdf->setImageScale(PDF_IMAGE_SCALE_RATIO); + + // set font + $pdf->setFont('helvetica', '', 10); + + // add a page + $pdf->AddPage(); + + $pdf->writeHTML($this->html, true, false, true, false, ''); + + //Close and output PDF document + return $pdf->Output('', 'S'); + } +} \ No newline at end of file diff --git a/app/ModelSerializers/SerializerRegistry.php b/app/ModelSerializers/SerializerRegistry.php index bbd04b28f..4be7e8c83 100644 --- a/app/ModelSerializers/SerializerRegistry.php +++ b/app/ModelSerializers/SerializerRegistry.php @@ -151,6 +151,7 @@ final class SerializerRegistry const SerializerType_Admin_Voteable_CSV = "ADMIN_VOTEABLE_CSV"; const SerializerType_CSV = 'CSV'; const SerializerType_Admin_Registration_Stats = 'ADMIN_REG_STATS'; + const SerializerType_Admin_Email_Preview = 'ADMIN_EMAIL_PREVIEW'; private function __clone() { @@ -429,6 +430,7 @@ private function __construct() $this->registry['SummitOrder'] = [ self::SerializerType_Public => SummitOrderBaseSerializer::class, + self::SerializerType_Admin_Email_Preview => SummitOrderConfirmationEmailPreviewSerializer::class, ISummitOrderSerializerTypes::CheckOutType => SummitOrderBaseSerializer::class, ISummitOrderSerializerTypes::ReservationType => SummitOrderReservationSerializer::class, ISummitOrderSerializerTypes::AdminType => SummitOrderAdminSerializer::class, diff --git a/app/ModelSerializers/Summit/Registration/SummitOrderConfirmationEmailPreviewSerializer.php b/app/ModelSerializers/Summit/Registration/SummitOrderConfirmationEmailPreviewSerializer.php new file mode 100644 index 000000000..6557133a5 --- /dev/null +++ b/app/ModelSerializers/Summit/Registration/SummitOrderConfirmationEmailPreviewSerializer.php @@ -0,0 +1,83 @@ + 'order_credit_card_type:json_string', + 'CreditCard4Number' => 'order_credit_card_4number:json_string', + 'Currency' => 'order_currency:json_string', + 'CurrencySymbol' => 'order_currency_symbol:json_string', + 'Number' => 'order_number:json_string', + 'FinalAmountAdjusted' => 'order_amount:json_float', + 'OwnerFullName' => 'owner_full_name:json_string', + 'OwnerCompanyName' => 'owner_company:json_string', + ]; + + protected static $allowed_relations = [ + 'member', + 'tickets', + ]; + + /** + * @param null $expand + * @param array $fields + * @param array $relations + * @param array $params + * @return array + */ + public function serialize($expand = null, array $fields = array(), array $relations = array(), array $params = array()) + { + $order = $this->object; + if (!$order instanceof SummitOrder) return []; + $values = parent::serialize($expand, $fields, $relations, $params); + + $values["summit_name"] = $order->getSummit()->getName(); + + if (!count($relations)) $relations = $this->getAllowedRelations(); + + if (in_array('tickets', $relations)) { + $tickets = []; + foreach ($order->getTickets() as $ticket) { + $ticket_dic = [ + "currency" => $ticket->getCurrency(), + "currency_symbol" => $ticket->getCurrencySymbol(), + "has_owner" => $ticket->hasOwner(), + "need_details" => false, + "ticket_type_name" => $ticket->getTicketTypeName(), + "owner_email" => $ticket->getOwnerEmail(), + "price" => $ticket->getFinalAmount() + ]; + + $promo_code = $ticket->getPromoCode(); + if (!is_null($promo_code)) { + $ticket_dic["promo_code"] = ["code" => $promo_code->getCode()]; + } + + $tickets[] = $ticket_dic; + } + $values['tickets'] = $tickets; + } + + return $values; + } +} \ No newline at end of file diff --git a/app/Services/Apis/IEmailTemplatesApi.php b/app/Services/Apis/IEmailTemplatesApi.php new file mode 100644 index 000000000..7ba839990 --- /dev/null +++ b/app/Services/Apis/IEmailTemplatesApi.php @@ -0,0 +1,35 @@ + $this->getAccessToken() + ]; + + $response = $this->client->get("/api/v1/mail-templates/{$template_id}", [ + 'query' => $query, + ] + ); + return json_decode($response->getBody()->getContents(), true); + } + catch (Exception $ex) { + $this->cleanAccessToken(); + Log::error($ex); + throw $ex; + } + } + + /** + * @param array $payload + * @param string $html_template + * @return mixed + * @throws \GuzzleHttp\Exception\GuzzleException|\League\OAuth2\Client\Provider\Exception\IdentityProviderException + */ + public function getEmailPreview(array $payload, string $html_template) + { + Log::debug("MailService::getEmailPreview"); + + try { + $query = [ + 'access_token' => $this->getAccessToken() + ]; + + $response = $this->client->put('/api/v1/mail-templates/all/render', [ + 'query' => $query, + RequestOptions::JSON => [ + "html" => $html_template, + "payload" => $payload + ] + ] + ); + return json_decode($response->getBody()->getContents(), true); + } + catch (Exception $ex) { + $this->cleanAccessToken(); + Log::error($ex); + throw $ex; + } + } } \ No newline at end of file diff --git a/app/Services/BaseServicesProvider.php b/app/Services/BaseServicesProvider.php index db726f00f..b0cc4fce9 100644 --- a/app/Services/BaseServicesProvider.php +++ b/app/Services/BaseServicesProvider.php @@ -15,12 +15,13 @@ use App\Permissions\PermissionsManager; use App\Services\Apis\ExternalUserApi; use App\Services\Apis\GoogleGeoCodingAPI; +use App\Services\Apis\IEmailTemplatesApi; use App\Services\Apis\IExternalUserApi; use App\Services\Apis\IGeoCodingAPI; use App\Services\Apis\IMailApi; use App\Services\Apis\IMUXApi; use App\Services\Apis\IPasswordlessAPI; -use App\Services\Apis\MailApi; +use App\Services\Apis\MailService; use App\Services\Apis\MUXApi; use App\Services\Apis\MuxCredentials; use App\Services\Apis\PasswordlessAPI; @@ -128,7 +129,12 @@ public function register() App::singleton( IMailApi::class, - MailApi::class + MailService::class + ); + + App::singleton( + IEmailTemplatesApi::class, + MailService::class ); App::singleton( @@ -183,6 +189,7 @@ public function provides() IPasswordlessAPI::class, ISamsungRegistrationAPI::class, IMUXApi::class, + IEmailTemplatesApi::class, ]; } } \ No newline at end of file diff --git a/app/Services/Model/ISummitOrderService.php b/app/Services/Model/ISummitOrderService.php index 357736765..d0b1f5cd7 100644 --- a/app/Services/Model/ISummitOrderService.php +++ b/app/Services/Model/ISummitOrderService.php @@ -11,6 +11,8 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ + +use Illuminate\Support\Facades\File; use models\exceptions\EntityNotFoundException; use models\exceptions\ValidationException; use models\main\Member; @@ -406,4 +408,11 @@ public function reSendOrderEmail(int $order_id):SummitOrder; */ public function cancelRequestRefundTicket(int $order_id, int $ticket_id, Member $currentUser, ?string $notes = null): SummitAttendeeTicket; + /** + * @param Summit $summit + * @param int $order_id + * @return string + * @throws \Exception + */ + public function renderOrderConfirmationEmail(Summit $summit, int $order_id): string; } \ No newline at end of file diff --git a/app/Services/Model/Imp/SummitOrderService.php b/app/Services/Model/Imp/SummitOrderService.php index baf122f62..51ef8a0e4 100644 --- a/app/Services/Model/Imp/SummitOrderService.php +++ b/app/Services/Model/Imp/SummitOrderService.php @@ -16,6 +16,7 @@ use App\Events\MemberUpdated; use App\Events\TicketUpdated; use App\Http\Renderers\SummitAttendeeTicketPDFRenderer; +use App\Http\Renderers\HTML2PDFRenderer; use App\Jobs\Emails\RegisteredMemberOrderPaidMail; use App\Jobs\Emails\Registration\Reminders\SummitOrderReminderEmail; use App\Jobs\Emails\Registration\Reminders\SummitTicketReminderEmail; @@ -27,6 +28,7 @@ use App\Models\Foundation\Summit\Repositories\ISummitAttendeeBadgePrintRuleRepository; use App\Models\Foundation\Summit\Repositories\ISummitAttendeeBadgeRepository; use App\Models\Foundation\Summit\Repositories\ISummitOrderRepository; +use App\Services\Apis\IEmailTemplatesApi; use App\Services\FileSystem\IFileDownloadStrategy; use App\Services\FileSystem\IFileUploadStrategy; use App\Services\Model\dto\ExternalUserDTO; @@ -63,6 +65,7 @@ use models\summit\SummitOrderExtraQuestionTypeConstants; use models\summit\SummitRegistrationPromoCode; use models\summit\SummitTicketType; +use ModelSerializers\SerializerRegistry; use utils\PagingInfo; /** @@ -1065,6 +1068,11 @@ final class SummitOrderService */ private $ticket_finder_strategy_factory; + /** + * @var IEmailTemplatesApi + */ + private $email_templates_api; + /** * @param ISummitTicketTypeRepository $ticket_type_repository * @param IMemberRepository $member_repository @@ -1084,6 +1092,7 @@ final class SummitOrderService * @param ITicketFinderStrategyFactory $ticket_finder_strategy_factory * @param ITransactionService $tx_service * @param ILockManagerService $lock_service + * @param IEmailTemplatesApi $email_templates_api */ public function __construct ( @@ -1104,7 +1113,8 @@ public function __construct ICompanyService $company_service, ITicketFinderStrategyFactory $ticket_finder_strategy_factory, ITransactionService $tx_service, - ILockManagerService $lock_service + ILockManagerService $lock_service, + IEmailTemplatesApi $email_templates_api ) { parent::__construct($tx_service); @@ -1125,6 +1135,7 @@ public function __construct $this->company_repository = $company_repository; $this->company_service = $company_service; $this->ticket_finder_strategy_factory = $ticket_finder_strategy_factory; + $this->email_templates_api = $email_templates_api; } /** @@ -4338,4 +4349,47 @@ public function deActivateTicket(Summit $summit, int $order_id, int $ticket_id): return $ticket; }); } + + /** + * @inheritDoc + */ + public function renderOrderConfirmationEmail(Summit $summit, int $order_id): string + { + try { + Log::debug("SummitOrderService::renderOrderConfirmationEmail order id {$order_id}"); + + $order = $summit->getOrderById($order_id); + + if (!$order instanceof SummitOrder) + throw new EntityNotFoundException("Order not found."); + + $payload = SerializerRegistry::getInstance() + ->getSerializer($order, SerializerRegistry::SerializerType_Admin_Email_Preview) + ->serialize(); + + $template_id = $summit->getEmailIdentifierPerEmailEventFlowSlug(RegisteredMemberOrderPaidMail::EVENT_SLUG); + + if (is_null($template_id)) + throw new EntityNotFoundException("Order confirmation email template not found."); + + $template = $this->email_templates_api->getEmailTemplate($template_id); + + if (is_null($template)) + throw new EntityNotFoundException("Could not retrieve the order confirmation email template."); + + $preview = $this->email_templates_api->getEmailPreview($payload, $template['html_content']); + + //Only the HTML body content is needed to create the PDF + preg_match("/]*>(.*?)<\/body>/is", $preview['html_content'], $matches); + + if (!is_null($matches) && count($matches) > 1) { + $renderer = new HTML2PDFRenderer(trim($matches[1])); + return $renderer->render(); + } + return ""; + } catch (\Exception $ex) { + Log::warning($ex); + throw $ex; + } + } } diff --git a/config/app.php b/config/app.php index 892329882..3668df4a8 100644 --- a/config/app.php +++ b/config/app.php @@ -224,5 +224,5 @@ 'app_name' => env('APP_NAME', 'Open Infrastructure Summit'), 'tenant_name' => env('TENANT_NAME', 'OpenStack'), - "default_profile_image" => env('DEFAULT_PROFILE_IMAGE', null), + 'default_profile_image' => env('DEFAULT_PROFILE_IMAGE', null), ]; diff --git a/database/seeders/ApiEndpointsSeeder.php b/database/seeders/ApiEndpointsSeeder.php index 0e9b95e92..5693acdbf 100644 --- a/database/seeders/ApiEndpointsSeeder.php +++ b/database/seeders/ApiEndpointsSeeder.php @@ -515,6 +515,21 @@ private function seedRegistrationOrderEndpoints() IGroup::SummitRegistrationAdmins ] ], + [ + 'name' => 'get-order-confirmation-email-pdf', + 'route' => '/api/v1/summits/{id}/orders/{order_id}/pdf', + 'http_method' => 'GET', + 'scopes' => [ + sprintf(SummitScopes::ReadAllSummitData, $current_realm), + sprintf(SummitScopes::ReadRegistrationOrders, $current_realm), + ], + 'authz_groups' => [ + IGroup::SuperAdmins, + IGroup::Administrators, + IGroup::SummitAdministrators, + IGroup::SummitRegistrationAdmins + ] + ], // purchase flow [ 'name' => 'reserve-registration-order', diff --git a/routes/api_v1.php b/routes/api_v1.php index 4b3096fc5..c45e0b395 100644 --- a/routes/api_v1.php +++ b/routes/api_v1.php @@ -1368,6 +1368,7 @@ Route::get('pdf', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitOrdersApiController@getTicketPDFBySummit']); }); }); + Route::get('pdf', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitOrdersApiController@getOrderConfirmationEmailPDF']); }); Route::post('reserve', 'OAuth2SummitOrdersApiController@reserve'); Route::group(['prefix' => '{hash}', 'where' => [ diff --git a/tests/OAuth2SummitOrdersApiTest.php b/tests/OAuth2SummitOrdersApiTest.php index c052d6aa2..04064b40e 100644 --- a/tests/OAuth2SummitOrdersApiTest.php +++ b/tests/OAuth2SummitOrdersApiTest.php @@ -18,6 +18,7 @@ use App\Models\Foundation\Summit\Factories\SummitTicketTypeFactory; use App\Models\Foundation\Summit\Factories\SummitBadgeTypeFactory; use services\model\ISummitService; +use TCPDF_STATIC; /** * Class OAuth2SummitOrdersApiTest @@ -930,4 +931,36 @@ public function testUpdateTicketById($ticket_id = 28){ $this->assertTrue(!is_null($order)); return $order; } + + /** + * @return mixed + */ + public function testGetOrderConfirmationEmailPDF(){ + $params = [ + 'id' => 3783, //self::$summit->getId(), + 'order_id' => 6658 + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "GET", + "OAuth2SummitOrdersApiController@getOrderConfirmationEmailPDF", + $params, + [], + [], + [], + $headers + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + + $f = TCPDF_STATIC::fopenLocal("/tmp/order_confirmation_email.pdf", 'wb'); + fwrite($f, $content, strlen($content)); + fclose($f); + } } \ No newline at end of file