Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 157 |
|
0.00% |
0 / 23 |
CRAP | |
0.00% |
0 / 1 |
| EmailNotificationService | |
0.00% |
0 / 157 |
|
0.00% |
0 / 23 |
992 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| sendAddFundsRequestEmail | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
| sendWithdrawalRequestEmail | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
| sendRequestApprovedEmail | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| sendRequestRejectedEmail | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| sendDocumentSignatureRequestEmail | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| sendDocumentSignedEmail | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
| sendMonthlyPerformanceEmail | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| sendStatementAvailableEmail | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| sendPasswordResetEmail | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| sendEmail | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
30 | |||
| buildAddFundsInvestorEmail | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
| buildAddFundsAdminEmail | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
| buildWithdrawalInvestorEmail | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
| buildWithdrawalAdminEmail | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
| buildApprovalEmail | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
| buildRejectionEmail | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
| buildSignatureRequestEmail | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
| buildDocumentSignedInvestorEmail | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
| buildDocumentSignedAdminEmail | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
| buildPerformanceEmail | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
| buildStatementEmail | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
| buildPasswordResetEmail | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace App\Domain\Email; |
| 6 | |
| 7 | use InvalidArgumentException; |
| 8 | |
| 9 | final readonly class EmailNotificationService |
| 10 | { |
| 11 | private const SENDGRID_API_URL = 'https://api.sendgrid.com/v3/mail/send'; |
| 12 | |
| 13 | private string $sendGridApiKey; |
| 14 | private string $senderEmail; |
| 15 | private string $senderName; |
| 16 | |
| 17 | public function __construct( |
| 18 | string $sendGridApiKey = '', |
| 19 | string $senderEmail = 'no-reply@apiaryfund.com', |
| 20 | string $senderName = 'Apiary Fund', |
| 21 | ) { |
| 22 | $this->sendGridApiKey = $sendGridApiKey; |
| 23 | $this->senderEmail = $senderEmail; |
| 24 | $this->senderName = $senderName; |
| 25 | } |
| 26 | |
| 27 | /** |
| 28 | * Send add funds request confirmation email |
| 29 | */ |
| 30 | public function sendAddFundsRequestEmail( |
| 31 | string $investorEmail, |
| 32 | string $investorName, |
| 33 | string $fundName, |
| 34 | string $amount, |
| 35 | string $wireReference, |
| 36 | string $adminEmail, |
| 37 | ): void { |
| 38 | $investorSubject = 'Add Funds Request Received - ' . $fundName; |
| 39 | $investorBody = $this->buildAddFundsInvestorEmail($investorName, $fundName, $amount, $wireReference); |
| 40 | |
| 41 | $this->sendEmail($investorEmail, $investorName, $investorSubject, $investorBody); |
| 42 | |
| 43 | $adminSubject = 'New Add Funds Request: ' . $investorName; |
| 44 | $adminBody = $this->buildAddFundsAdminEmail($investorName, $investorEmail, $fundName, $amount, $wireReference); |
| 45 | |
| 46 | $this->sendEmail($adminEmail, 'Admin', $adminSubject, $adminBody); |
| 47 | } |
| 48 | |
| 49 | /** |
| 50 | * Send withdrawal request confirmation email |
| 51 | */ |
| 52 | public function sendWithdrawalRequestEmail( |
| 53 | string $investorEmail, |
| 54 | string $investorName, |
| 55 | string $fundName, |
| 56 | string $amount, |
| 57 | ?string $reason, |
| 58 | string $adminEmail, |
| 59 | ): void { |
| 60 | $investorSubject = 'Withdrawal Request Received - ' . $fundName; |
| 61 | $investorBody = $this->buildWithdrawalInvestorEmail($investorName, $fundName, $amount, $reason); |
| 62 | |
| 63 | $this->sendEmail($investorEmail, $investorName, $investorSubject, $investorBody); |
| 64 | |
| 65 | $adminSubject = 'New Withdrawal Request: ' . $investorName; |
| 66 | $adminBody = $this->buildWithdrawalAdminEmail($investorName, $investorEmail, $fundName, $amount, $reason); |
| 67 | |
| 68 | $this->sendEmail($adminEmail, 'Admin', $adminSubject, $adminBody); |
| 69 | } |
| 70 | |
| 71 | /** |
| 72 | * Send request approval email |
| 73 | */ |
| 74 | public function sendRequestApprovedEmail( |
| 75 | string $investorEmail, |
| 76 | string $investorName, |
| 77 | string $fundName, |
| 78 | string $amount, |
| 79 | string $type, |
| 80 | ): void { |
| 81 | $subject = 'Request Approved - ' . $fundName; |
| 82 | $body = $this->buildApprovalEmail($investorName, $fundName, $amount, $type); |
| 83 | |
| 84 | $this->sendEmail($investorEmail, $investorName, $subject, $body); |
| 85 | } |
| 86 | |
| 87 | /** |
| 88 | * Send request rejection email |
| 89 | */ |
| 90 | public function sendRequestRejectedEmail( |
| 91 | string $investorEmail, |
| 92 | string $investorName, |
| 93 | string $fundName, |
| 94 | string $amount, |
| 95 | ?string $reason, |
| 96 | string $contactEmail, |
| 97 | ): void { |
| 98 | $subject = 'Request Rejected - ' . $fundName; |
| 99 | $body = $this->buildRejectionEmail($investorName, $fundName, $amount, $reason, $contactEmail); |
| 100 | |
| 101 | $this->sendEmail($investorEmail, $investorName, $subject, $body); |
| 102 | } |
| 103 | |
| 104 | /** |
| 105 | * Send document signature request email |
| 106 | */ |
| 107 | public function sendDocumentSignatureRequestEmail( |
| 108 | string $investorEmail, |
| 109 | string $investorName, |
| 110 | string $documentName, |
| 111 | string $fundName, |
| 112 | string $signLink, |
| 113 | ): void { |
| 114 | $subject = 'Action Required: Sign Document - ' . $documentName; |
| 115 | $body = $this->buildSignatureRequestEmail($investorName, $documentName, $fundName, $signLink); |
| 116 | |
| 117 | $this->sendEmail($investorEmail, $investorName, $subject, $body); |
| 118 | } |
| 119 | |
| 120 | /** |
| 121 | * Send document signed confirmation |
| 122 | */ |
| 123 | public function sendDocumentSignedEmail( |
| 124 | string $investorEmail, |
| 125 | string $investorName, |
| 126 | string $documentName, |
| 127 | string $adminEmail, |
| 128 | ): void { |
| 129 | $investorSubject = 'Document Signed: ' . $documentName; |
| 130 | $investorBody = $this->buildDocumentSignedInvestorEmail($investorName, $documentName); |
| 131 | |
| 132 | $this->sendEmail($investorEmail, $investorName, $investorSubject, $investorBody); |
| 133 | |
| 134 | $adminSubject = 'Document Signed by ' . $investorName; |
| 135 | $adminBody = $this->buildDocumentSignedAdminEmail($investorName, $documentName); |
| 136 | |
| 137 | $this->sendEmail($adminEmail, 'Admin', $adminSubject, $adminBody); |
| 138 | } |
| 139 | |
| 140 | /** |
| 141 | * Send monthly performance email |
| 142 | */ |
| 143 | public function sendMonthlyPerformanceEmail( |
| 144 | string $investorEmail, |
| 145 | string $investorName, |
| 146 | string $fundName, |
| 147 | string $monthlyReturn, |
| 148 | string $ytdReturn, |
| 149 | string $dashboardUrl, |
| 150 | ): void { |
| 151 | $subject = 'Monthly Performance Update - ' . $fundName; |
| 152 | $body = $this->buildPerformanceEmail($investorName, $fundName, $monthlyReturn, $ytdReturn, $dashboardUrl); |
| 153 | |
| 154 | $this->sendEmail($investorEmail, $investorName, $subject, $body); |
| 155 | } |
| 156 | |
| 157 | /** |
| 158 | * Send statement available email |
| 159 | */ |
| 160 | public function sendStatementAvailableEmail( |
| 161 | string $investorEmail, |
| 162 | string $investorName, |
| 163 | string $period, |
| 164 | string $statementType, |
| 165 | string $downloadLink, |
| 166 | ): void { |
| 167 | $subject = 'Your ' . $period . ' ' . $statementType . ' is Ready'; |
| 168 | $body = $this->buildStatementEmail($investorName, $period, $statementType, $downloadLink); |
| 169 | |
| 170 | $this->sendEmail($investorEmail, $investorName, $subject, $body); |
| 171 | } |
| 172 | |
| 173 | /** |
| 174 | * Send password reset email |
| 175 | */ |
| 176 | public function sendPasswordResetEmail( |
| 177 | string $investorEmail, |
| 178 | string $investorName, |
| 179 | string $resetLink, |
| 180 | ): void { |
| 181 | $subject = 'Password Reset Request'; |
| 182 | $body = $this->buildPasswordResetEmail($investorName, $resetLink); |
| 183 | |
| 184 | $this->sendEmail($investorEmail, $investorName, $subject, $body); |
| 185 | } |
| 186 | |
| 187 | private function sendEmail( |
| 188 | string $toEmail, |
| 189 | string $toName, |
| 190 | string $subject, |
| 191 | string $htmlBody, |
| 192 | ): void { |
| 193 | // Skip sending emails if API key is not configured (development mode) |
| 194 | if (empty($this->sendGridApiKey)) { |
| 195 | return; |
| 196 | } |
| 197 | |
| 198 | $payload = [ |
| 199 | 'personalizations' => [ |
| 200 | [ |
| 201 | 'to' => [ |
| 202 | ['email' => $toEmail, 'name' => $toName], |
| 203 | ], |
| 204 | 'subject' => $subject, |
| 205 | ], |
| 206 | ], |
| 207 | 'from' => [ |
| 208 | 'email' => $this->senderEmail, |
| 209 | 'name' => $this->senderName, |
| 210 | ], |
| 211 | 'content' => [ |
| 212 | [ |
| 213 | 'type' => 'text/html', |
| 214 | 'value' => $htmlBody, |
| 215 | ], |
| 216 | ], |
| 217 | ]; |
| 218 | |
| 219 | $ch = curl_init(self::SENDGRID_API_URL); |
| 220 | if ($ch === false) { |
| 221 | throw new InvalidArgumentException('Failed to initialize cURL'); |
| 222 | } |
| 223 | |
| 224 | curl_setopt_array($ch, [ |
| 225 | CURLOPT_POST => true, |
| 226 | CURLOPT_POSTFIELDS => json_encode($payload), |
| 227 | CURLOPT_HTTPHEADER => [ |
| 228 | 'Authorization: Bearer ' . $this->sendGridApiKey, |
| 229 | 'Content-Type: application/json', |
| 230 | ], |
| 231 | CURLOPT_RETURNTRANSFER => true, |
| 232 | CURLOPT_TIMEOUT => 10, |
| 233 | ]); |
| 234 | |
| 235 | $response = curl_exec($ch); |
| 236 | $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); |
| 237 | curl_close($ch); |
| 238 | |
| 239 | if ($httpCode < 200 || $httpCode >= 300) { |
| 240 | throw new InvalidArgumentException('SendGrid API error: HTTP ' . $httpCode); |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | private function buildAddFundsInvestorEmail(string $name, string $fund, string $amount, string $reference): string |
| 245 | { |
| 246 | return <<<HTML |
| 247 | <!DOCTYPE html> |
| 248 | <html> |
| 249 | <head> |
| 250 | <style> |
| 251 | body { font-family: 'Libre Franklin', Arial, sans-serif; color: #333B45; line-height: 1.6; } |
| 252 | .header { background: linear-gradient(135deg, #4A90C4 0%, #2E6A9A 100%); color: white; padding: 32px; text-align: center; } |
| 253 | .content { max-width: 600px; margin: 0 auto; padding: 32px; } |
| 254 | .metric { background: #F4F5F7; padding: 16px; margin: 16px 0; border-radius: 8px; } |
| 255 | .button { background: #4A90C4; color: white; padding: 12px 24px; border-radius: 6px; text-decoration: none; display: inline-block; margin-top: 16px; } |
| 256 | .footer { background: #F4F5F7; padding: 24px; text-align: center; font-size: 12px; color: #5A6472; } |
| 257 | </style> |
| 258 | </head> |
| 259 | <body> |
| 260 | <div class="header"> |
| 261 | <h1>Add Funds Request Received</h1> |
| 262 | </div> |
| 263 | <div class="content"> |
| 264 | <p>Hi $name,</p> |
| 265 | <p>Thank you for your add funds request. We've received the following details:</p> |
| 266 | <div class="metric"> |
| 267 | <strong>Fund:</strong> $fund<br> |
| 268 | <strong>Amount:</strong> $$amount<br> |
| 269 | <strong>Wire Reference:</strong> <code>$reference</code><br> |
| 270 | </div> |
| 271 | <p><strong>Next Steps:</strong></p> |
| 272 | <ul> |
| 273 | <li>Please include the reference code in your wire transfer</li> |
| 274 | <li>Funds typically settle within 2-3 business days</li> |
| 275 | <li>You'll receive email confirmation once processed</li> |
| 276 | </ul> |
| 277 | <a href="https://portal.apiarycapital.com/dashboard" class="button">View Dashboard</a> |
| 278 | <p>If you have any questions, please contact our investor relations team.</p> |
| 279 | </div> |
| 280 | <div class="footer"> |
| 281 | <p>© 2024 Apiary Capital. All rights reserved.</p> |
| 282 | </div> |
| 283 | </body> |
| 284 | </html> |
| 285 | HTML; |
| 286 | } |
| 287 | |
| 288 | private function buildAddFundsAdminEmail(string $name, string $email, string $fund, string $amount, string $reference): string |
| 289 | { |
| 290 | return <<<HTML |
| 291 | <!DOCTYPE html> |
| 292 | <html> |
| 293 | <head> |
| 294 | <style> |
| 295 | body { font-family: 'Libre Franklin', Arial, sans-serif; color: #333B45; line-height: 1.6; } |
| 296 | .header { background: linear-gradient(135deg, #4A90C4 0%, #2E6A9A 100%); color: white; padding: 32px; text-align: center; } |
| 297 | .content { max-width: 600px; margin: 0 auto; padding: 32px; } |
| 298 | .metric { background: #F4F5F7; padding: 16px; margin: 16px 0; border-radius: 8px; } |
| 299 | .button { background: #4A90C4; color: white; padding: 12px 24px; border-radius: 6px; text-decoration: none; display: inline-block; } |
| 300 | </style> |
| 301 | </head> |
| 302 | <body> |
| 303 | <div class="header"> |
| 304 | <h1>New Add Funds Request</h1> |
| 305 | </div> |
| 306 | <div class="content"> |
| 307 | <p>New add funds request received:</p> |
| 308 | <div class="metric"> |
| 309 | <strong>Investor:</strong> $name ($email)<br> |
| 310 | <strong>Fund:</strong> $fund<br> |
| 311 | <strong>Amount:</strong> $$amount<br> |
| 312 | <strong>Wire Reference:</strong> <code>$reference</code><br> |
| 313 | </div> |
| 314 | <a href="https://portal.apiarycapital.com/admin/requests" class="button">Review Request</a> |
| 315 | </div> |
| 316 | </body> |
| 317 | </html> |
| 318 | HTML; |
| 319 | } |
| 320 | |
| 321 | private function buildWithdrawalInvestorEmail(string $name, string $fund, string $amount, ?string $reason): string |
| 322 | { |
| 323 | $reasonHtml = $reason ? "<strong>Reason:</strong> $reason<br>" : ''; |
| 324 | return <<<HTML |
| 325 | <!DOCTYPE html> |
| 326 | <html> |
| 327 | <head> |
| 328 | <style> |
| 329 | body { font-family: 'Libre Franklin', Arial, sans-serif; color: #333B45; line-height: 1.6; } |
| 330 | .header { background: linear-gradient(135deg, #4A90C4 0%, #2E6A9A 100%); color: white; padding: 32px; text-align: center; } |
| 331 | .content { max-width: 600px; margin: 0 auto; padding: 32px; } |
| 332 | .metric { background: #FEF3C7; padding: 16px; margin: 16px 0; border-radius: 8px; border-left: 4px solid #D97706; } |
| 333 | </style> |
| 334 | </head> |
| 335 | <body> |
| 336 | <div class="header"> |
| 337 | <h1>Withdrawal Request Received</h1> |
| 338 | </div> |
| 339 | <div class="content"> |
| 340 | <p>Hi $name,</p> |
| 341 | <p>We've received your withdrawal request:</p> |
| 342 | <div class="metric"> |
| 343 | <strong>Fund:</strong> $fund<br> |
| 344 | <strong>Amount:</strong> $$amount<br> |
| 345 | $reasonHtml |
| 346 | <strong>Status:</strong> Pending Admin Review |
| 347 | </div> |
| 348 | <p><strong>Processing Timeline:</strong> 5-10 business days after approval</p> |
| 349 | <p>You'll receive email notification once approved or rejected.</p> |
| 350 | </div> |
| 351 | </body> |
| 352 | </html> |
| 353 | HTML; |
| 354 | } |
| 355 | |
| 356 | private function buildWithdrawalAdminEmail(string $name, string $email, string $fund, string $amount, ?string $reason): string |
| 357 | { |
| 358 | $reasonHtml = $reason ? "<strong>Reason:</strong> $reason<br>" : ''; |
| 359 | return <<<HTML |
| 360 | <!DOCTYPE html> |
| 361 | <html> |
| 362 | <body> |
| 363 | <p>New withdrawal request from $name ($email):</p> |
| 364 | <p>Fund: $fund<br>Amount: $$amount<br>$reasonHtml</p> |
| 365 | <a href="https://portal.apiarycapital.com/admin/requests">Review Request</a> |
| 366 | </body> |
| 367 | </html> |
| 368 | HTML; |
| 369 | } |
| 370 | |
| 371 | private function buildApprovalEmail(string $name, string $fund, string $amount, string $type): string |
| 372 | { |
| 373 | $message = $type === 'add_funds' |
| 374 | ? "Your deposit of $$amount has been approved and will be reflected in your account shortly." |
| 375 | : "Your withdrawal request of $$amount has been approved. Wire transfer will be initiated."; |
| 376 | |
| 377 | return <<<HTML |
| 378 | <!DOCTYPE html> |
| 379 | <html> |
| 380 | <body> |
| 381 | <h2>Request Approved</h2> |
| 382 | <p>Hi $name,</p> |
| 383 | <p>Your request for $fund has been approved.</p> |
| 384 | <p>$message</p> |
| 385 | <a href="https://portal.apiarycapital.com/dashboard">View Dashboard</a> |
| 386 | </body> |
| 387 | </html> |
| 388 | HTML; |
| 389 | } |
| 390 | |
| 391 | private function buildRejectionEmail(string $name, string $fund, string $amount, ?string $reason, string $contactEmail): string |
| 392 | { |
| 393 | $reasonHtml = $reason ? "<p><strong>Reason:</strong> $reason</p>" : ''; |
| 394 | return <<<HTML |
| 395 | <!DOCTYPE html> |
| 396 | <html> |
| 397 | <body> |
| 398 | <h2>Request Rejected</h2> |
| 399 | <p>Hi $name,</p> |
| 400 | <p>Your request for $fund ($$amount) has been rejected.</p> |
| 401 | $reasonHtml |
| 402 | <p>Please contact us at <a href="mailto:$contactEmail">$contactEmail</a> for more information.</p> |
| 403 | </body> |
| 404 | </html> |
| 405 | HTML; |
| 406 | } |
| 407 | |
| 408 | private function buildSignatureRequestEmail(string $name, string $document, string $fund, string $signLink): string |
| 409 | { |
| 410 | return <<<HTML |
| 411 | <!DOCTYPE html> |
| 412 | <html> |
| 413 | <body> |
| 414 | <h2>Action Required: Sign Document</h2> |
| 415 | <p>Hi $name,</p> |
| 416 | <p>$document for $fund requires your signature.</p> |
| 417 | <a href="$signLink" style="background: #4A90C4; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">Sign Document</a> |
| 418 | <p>This action is required to proceed with your investment.</p> |
| 419 | </body> |
| 420 | </html> |
| 421 | HTML; |
| 422 | } |
| 423 | |
| 424 | private function buildDocumentSignedInvestorEmail(string $name, string $document): string |
| 425 | { |
| 426 | return <<<HTML |
| 427 | <!DOCTYPE html> |
| 428 | <html> |
| 429 | <body> |
| 430 | <h2>Document Signed</h2> |
| 431 | <p>Hi $name,</p> |
| 432 | <p>$document has been successfully signed. A signed copy has been sent to your email.</p> |
| 433 | <a href="https://portal.apiarycapital.com/documents">View All Documents</a> |
| 434 | </body> |
| 435 | </html> |
| 436 | HTML; |
| 437 | } |
| 438 | |
| 439 | private function buildDocumentSignedAdminEmail(string $name, string $document): string |
| 440 | { |
| 441 | return <<<HTML |
| 442 | <!DOCTYPE html> |
| 443 | <html> |
| 444 | <body> |
| 445 | <p>Document Signed Notification</p> |
| 446 | <p>$name has signed: $document</p> |
| 447 | </body> |
| 448 | </html> |
| 449 | HTML; |
| 450 | } |
| 451 | |
| 452 | private function buildPerformanceEmail(string $name, string $fund, string $monthly, string $ytd, string $dashboardUrl): string |
| 453 | { |
| 454 | return <<<HTML |
| 455 | <!DOCTYPE html> |
| 456 | <html> |
| 457 | <body> |
| 458 | <h2>Monthly Performance Update</h2> |
| 459 | <p>Hi $name,</p> |
| 460 | <p>$fund Performance:</p> |
| 461 | <p>Monthly Return: $monthly<br>YTD Return: $ytd</p> |
| 462 | <a href="$dashboardUrl">View Dashboard</a> |
| 463 | </body> |
| 464 | </html> |
| 465 | HTML; |
| 466 | } |
| 467 | |
| 468 | private function buildStatementEmail(string $name, string $period, string $type, string $downloadLink): string |
| 469 | { |
| 470 | return <<<HTML |
| 471 | <!DOCTYPE html> |
| 472 | <html> |
| 473 | <body> |
| 474 | <h2>Your Statement is Ready</h2> |
| 475 | <p>Hi $name,</p> |
| 476 | <p>Your $period $type is now available.</p> |
| 477 | <a href="$downloadLink">Download Statement</a> |
| 478 | </body> |
| 479 | </html> |
| 480 | HTML; |
| 481 | } |
| 482 | |
| 483 | private function buildPasswordResetEmail(string $name, string $resetLink): string |
| 484 | { |
| 485 | return <<<HTML |
| 486 | <!DOCTYPE html> |
| 487 | <html> |
| 488 | <body> |
| 489 | <h2>Password Reset Request</h2> |
| 490 | <p>Hi $name,</p> |
| 491 | <p><a href="$resetLink">Click here to reset your password</a></p> |
| 492 | <p>This link expires in 1 hour. If you didn't request this, please ignore this email.</p> |
| 493 | </body> |
| 494 | </html> |
| 495 | HTML; |
| 496 | } |
| 497 | } |