Skip to content

Commit aa5daec

Browse files
authored
Merge pull request #100 from johntrickett86/feat-email-invoices
Add invoice emailing functionality and tests
2 parents 413866b + eccbcef commit aa5daec

2 files changed

Lines changed: 581 additions & 0 deletions

File tree

src/Resources/Invoices.php

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
namespace Dcblogdev\Xero\Resources;
66

77
use Dcblogdev\Xero\Enums\FilterOptions;
8+
use Dcblogdev\Xero\Enums\InvoiceStatus;
9+
use Dcblogdev\Xero\Enums\InvoiceType;
810
use Dcblogdev\Xero\Xero;
11+
use Exception;
12+
use Illuminate\Http\Client\RequestException;
13+
use Illuminate\Support\Facades\Http;
914
use InvalidArgumentException;
1015

1116
class Invoices extends Xero
@@ -76,4 +81,147 @@ public function attachment(string $invoiceId, ?string $attachmentId = null, ?str
7681

7782
return $result['body'];
7883
}
84+
85+
/**
86+
* Email an invoice to the contact's primary email and any contact persons with IncludeInEmails flag set to true.
87+
* The invoice must be of Type ACCREC and a valid Status for sending (SUBMITTED, AUTHORISED or PAID).
88+
*
89+
* @param string $invoiceId The invoice ID to email
90+
* @return array Returns an array with status code, success flag, and any messages/errors:
91+
* - 'status': HTTP status code (204, 400, etc.)
92+
* - 'success': boolean indicating if the email was sent successfully
93+
* - 'message': optional success message
94+
* - 'errors': optional array of error details
95+
* - 'body': optional response body
96+
*
97+
* @throws Exception
98+
*/
99+
public function email(string $invoiceId): array
100+
{
101+
try {
102+
$response = Http::withToken($this->getAccessToken())
103+
->withHeaders(['Xero-tenant-id' => $this->getTenantId()])
104+
->accept('application/json')
105+
->post('https://api.xero.com/api.xro/2.0/Invoices/'.$invoiceId.'/Email', []);
106+
107+
$statusCode = $response->status();
108+
$body = $response->json() ?? [];
109+
110+
// For 204 No Content (success)
111+
if ($statusCode === 204) {
112+
return [
113+
'status' => 204,
114+
'success' => true,
115+
'message' => 'Invoice email sent successfully',
116+
'body' => [],
117+
];
118+
}
119+
120+
// For 400 errors, return structured error information
121+
if ($statusCode === 400) {
122+
return [
123+
'status' => 400,
124+
'success' => false,
125+
'errors' => $body,
126+
'message' => $body['Message'] ?? $body['Detail'] ?? 'Invoice email failed',
127+
'body' => $body,
128+
];
129+
}
130+
131+
// For other errors, throw exception
132+
$response->throw();
133+
134+
// This should never be reached, but PHPStan requires a return
135+
return [];
136+
} catch (RequestException $e) {
137+
$statusCode = $e->response->status();
138+
$body = $e->response->json() ?? [];
139+
140+
// For 400 errors, return structured error information
141+
if ($statusCode === 400) {
142+
return [
143+
'status' => 400,
144+
'success' => false,
145+
'errors' => $body,
146+
'message' => $body['Message'] ?? $body['Detail'] ?? 'Invoice email failed',
147+
'body' => $body,
148+
];
149+
}
150+
151+
// For other errors, throw exception as usual
152+
$response = json_decode($e->response->body());
153+
throw new Exception($response->Detail ?? "Type: $response?->Type Message: $response?->Message Error Number: $response?->ErrorNumber");
154+
} catch (Exception $e) {
155+
throw new Exception($e->getMessage());
156+
}
157+
}
158+
159+
/**
160+
* Get the list of email addresses that will receive the invoice email.
161+
* Returns the primary contact email and any contact persons with IncludeInEmails flag set to true.
162+
*
163+
* @param string $invoiceId The invoice ID
164+
* @return array<string> Array of email addresses
165+
*
166+
* @throws Exception
167+
*/
168+
public function getEmailRecipients(string $invoiceId): array
169+
{
170+
$invoice = $this->find($invoiceId);
171+
$contactId = $invoice['Contact']['ContactID'] ?? null;
172+
173+
if (! $contactId) {
174+
return [];
175+
}
176+
177+
$contact = $this->contacts()->find($contactId);
178+
$recipients = [];
179+
180+
// Add primary contact email if it exists
181+
if (! empty($contact['EmailAddress'])) {
182+
$recipients[] = $contact['EmailAddress'];
183+
}
184+
185+
// Add contact persons with IncludeInEmails flag set to true
186+
if (! empty($contact['ContactPersons']) && is_array($contact['ContactPersons'])) {
187+
foreach ($contact['ContactPersons'] as $contactPerson) {
188+
if (isset($contactPerson['IncludeInEmails']) && $contactPerson['IncludeInEmails'] === true) {
189+
if (! empty($contactPerson['EmailAddress'])) {
190+
$recipients[] = $contactPerson['EmailAddress'];
191+
}
192+
}
193+
}
194+
}
195+
196+
return array_unique($recipients);
197+
}
198+
199+
/**
200+
* Check if an invoice can be emailed.
201+
* The invoice must be of Type ACCREC and have a Status of SUBMITTED, AUTHORISED, or PAID.
202+
*
203+
* @param string $invoiceId The invoice ID to check
204+
* @return bool Returns true if the invoice can be emailed, false otherwise
205+
*
206+
* @throws Exception
207+
*/
208+
public function canEmail(string $invoiceId): bool
209+
{
210+
$invoice = $this->find($invoiceId);
211+
212+
// Invoice must be Type ACCREC
213+
if (($invoice['Type'] ?? '') !== InvoiceType::AccRec->value) {
214+
return false;
215+
}
216+
217+
// Invoice must have a valid status for sending
218+
$status = $invoice['Status'] ?? '';
219+
$validStatuses = [
220+
InvoiceStatus::Submitted->value,
221+
InvoiceStatus::Authorised->value,
222+
InvoiceStatus::Paid->value,
223+
];
224+
225+
return in_array($status, $validStatuses, true);
226+
}
79227
}

0 commit comments

Comments
 (0)