Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 157
0.00% covered (danger)
0.00%
0 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 1
EmailNotificationService
0.00% covered (danger)
0.00%
0 / 157
0.00% covered (danger)
0.00%
0 / 23
992
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 sendAddFundsRequestEmail
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 sendWithdrawalRequestEmail
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 sendRequestApprovedEmail
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 sendRequestRejectedEmail
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 sendDocumentSignatureRequestEmail
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 sendDocumentSignedEmail
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 sendMonthlyPerformanceEmail
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 sendStatementAvailableEmail
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 sendPasswordResetEmail
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 sendEmail
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
30
 buildAddFundsInvestorEmail
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 buildAddFundsAdminEmail
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 buildWithdrawalInvestorEmail
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 buildWithdrawalAdminEmail
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 buildApprovalEmail
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 buildRejectionEmail
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 buildSignatureRequestEmail
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 buildDocumentSignedInvestorEmail
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 buildDocumentSignedAdminEmail
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 buildPerformanceEmail
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 buildStatementEmail
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 buildPasswordResetEmail
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace App\Domain\Email;
6
7use InvalidArgumentException;
8
9final 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>&copy; 2024 Apiary Capital. All rights reserved.</p>
282    </div>
283</body>
284</html>
285HTML;
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>
318HTML;
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>
353HTML;
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>
368HTML;
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>
388HTML;
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>
405HTML;
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>
421HTML;
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>
436HTML;
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>
449HTML;
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>
465HTML;
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>
480HTML;
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>
495HTML;
496    }
497}