Skip to content

Commit 70ba906

Browse files
authored
Merge pull request #114 from allanstone/send-email
Add send-by-email method
2 parents 4f39663 + 26bc063 commit 70ba906

File tree

7 files changed

+360
-2
lines changed

7 files changed

+360
-2
lines changed

facturapi/resources/invoices.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class to create the resource and a class to represent an
1111
from pydantic import BaseModel
1212
from pydantic.dataclasses import dataclass
1313

14+
from ..http import client
1415
from ..types import InvoiceRelation, InvoiceUse, PaymentForm, PaymentMethod
1516
from ..types.general import (
1617
CustomerBasicInfo,
@@ -191,6 +192,38 @@ def cancel(cls, invoice_id: str, motive: str) -> 'Invoice':
191192
"""
192193
return cast('Invoice', cls._delete(invoice_id, **dict(motive=motive)))
193194

195+
@classmethod
196+
def send_by_email(
197+
cls,
198+
invoice_id: str,
199+
recipients: Optional[Union[str, List[str]]] = None,
200+
) -> bool:
201+
"""Send an invoice by email.
202+
203+
Sends an email to the customer's email address with the XML and PDF
204+
files attached.
205+
206+
Args:
207+
invoice_id: The ID of the invoice to send.
208+
recipients: The email addresses to send the invoice to.
209+
If not provided, the invoice will be sent to the customer's
210+
registered email.
211+
212+
Returns:
213+
bool: True if the email was sent successfully, False otherwise.
214+
215+
Raises:
216+
FacturapiResponseException: If the invoice_id is not found.
217+
requests.RequestException: If the API request fails.
218+
"""
219+
220+
endpoint = f"{cls._resource}/{invoice_id}/email"
221+
payload = {}
222+
if recipients:
223+
payload["email"] = recipients
224+
response = client.post(endpoint, payload)
225+
return response.get("ok", False)
226+
194227
@property
195228
def customer(self) -> Customer:
196229
"""Fetch and access Customer resource.

facturapi/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
__version__ = '0.1.1' # pragma: no cover
1+
__version__ = '0.2.0' # pragma: no cover
22
CLIENT_VERSION = __version__
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
interactions:
2+
- request:
3+
body: "{}"
4+
headers:
5+
Accept:
6+
- "*/*"
7+
Accept-Encoding:
8+
- gzip, deflate
9+
Authorization:
10+
- DUMMY
11+
Connection:
12+
- keep-alive
13+
Content-Length:
14+
- "30"
15+
Content-Type:
16+
- application/json
17+
User-Agent:
18+
- facturapi-python/0.1.2.dev0
19+
method: POST
20+
uri: https://www.facturapi.io/v2/invoices/INVOICE01/email
21+
response:
22+
body:
23+
string: '{"ok":false}'
24+
headers:
25+
Content-Length:
26+
- "11"
27+
access-control-allow-origin:
28+
- "*"
29+
content-security-policy:
30+
- "default-src 'self';font-src https:;img-src data: https:;script-src 'unsafe-inline'
31+
https:;style-src https: 'unsafe-inline';object-src 'none';connect-src
32+
https:;frame-src self *.stripe.com *.facebook.com;upgrade-insecure-requests"
33+
content-type:
34+
- application/json; charset=utf-8
35+
cross-origin-opener-policy:
36+
- same-origin-allow-popups
37+
cross-origin-resource-policy:
38+
- same-site
39+
date:
40+
- Thu, 27 Mar 2025 19:45:43 GMT
41+
etag:
42+
- W/"b-Ai2R8hgEarLmHKwesT1qcY913ys"
43+
origin-agent-cluster:
44+
- ?1
45+
referrer-policy:
46+
- no-referrer
47+
server:
48+
- Google Frontend
49+
strict-transport-security:
50+
- max-age=15552000; includeSubDomains
51+
x-cloud-trace-context:
52+
- a37cf4f44f040b2436c140d1010f163d
53+
x-content-type-options:
54+
- nosniff
55+
x-dns-prefetch-control:
56+
- "off"
57+
x-download-options:
58+
- noopen
59+
x-facturapi-log-id:
60+
- 67e5aae72f593170271effbc
61+
x-frame-options:
62+
- SAMEORIGIN
63+
x-permitted-cross-domain-policies:
64+
- none
65+
x-xss-protection:
66+
- "0"
67+
status:
68+
code: 200
69+
message: OK
70+
version: 1
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
interactions:
2+
- request:
3+
body: '{}'
4+
headers:
5+
Accept:
6+
- '*/*'
7+
Accept-Encoding:
8+
- gzip, deflate
9+
Authorization:
10+
- DUMMY
11+
Connection:
12+
- keep-alive
13+
Content-Length:
14+
- '2'
15+
Content-Type:
16+
- application/json
17+
User-Agent:
18+
- facturapi-python/0.2.0
19+
method: POST
20+
uri: https://www.facturapi.io/v2/invoices/INVOICE_NOT_FOUND/email
21+
response:
22+
body:
23+
string: '{"message":"Invoice with Id \"INVOICE_NOT_FOUND\" was not found or
24+
you don''t have the right permissions","ok":false}'
25+
headers:
26+
Content-Length:
27+
- '116'
28+
access-control-allow-origin:
29+
- '*'
30+
content-security-policy:
31+
- 'default-src ''self'' https://accounts.google.com/gsi/* https://*.stripe.com;font-src
32+
https:;img-src data: https: https://*.stripe.com;script-src ''unsafe-inline''
33+
https: https://accounts.google.com/gsi/client https://builder.io https://builder.io/*;style-src
34+
https: ''unsafe-inline'' https://accounts.google.com/gsi/style;base-uri ''self'';object-src
35+
''none'';connect-src https: https://accounts.google.com/gsi/* https://*.stripe.com;frame-src
36+
''self'' *.stripe.com *.facebook.com https://accounts.google.com/ https://accounts.google.com/gsi/
37+
https://accounts.google.com/gsi/* https://builder.io https://builder.io/*;frame-ancestors
38+
''self'' https://builder.io https://builder.io/*;upgrade-insecure-requests;form-action
39+
''self'';script-src-attr ''none'''
40+
content-type:
41+
- application/json; charset=utf-8
42+
cross-origin-opener-policy:
43+
- same-origin-allow-popups
44+
cross-origin-resource-policy:
45+
- same-site
46+
date:
47+
- Fri, 04 Apr 2025 02:59:28 GMT
48+
etag:
49+
- W/"74-9U2JIE0F589sz9k6Zf4ELoiLoa4"
50+
origin-agent-cluster:
51+
- ?1
52+
referrer-policy:
53+
- no-referrer
54+
server:
55+
- Google Frontend
56+
strict-transport-security:
57+
- max-age=15552000; includeSubDomains
58+
x-cloud-trace-context:
59+
- 34e5c1b94a06826421ee52ae05c02139/10885945258051611867;o=1
60+
x-content-type-options:
61+
- nosniff
62+
x-dns-prefetch-control:
63+
- 'off'
64+
x-download-options:
65+
- noopen
66+
x-facturapi-log-id:
67+
- 67ef4b105eb1f5f4a053ae9f
68+
x-frame-options:
69+
- SAMEORIGIN
70+
x-permitted-cross-domain-policies:
71+
- none
72+
x-xss-protection:
73+
- '0'
74+
status:
75+
code: 400
76+
message: Bad Request
77+
version: 1
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
interactions:
2+
- request:
3+
body: '{"email": "frida_kahlo@test.com"}'
4+
headers:
5+
Accept:
6+
- "*/*"
7+
Accept-Encoding:
8+
- gzip, deflate
9+
Authorization:
10+
- DUMMY
11+
Connection:
12+
- keep-alive
13+
Content-Length:
14+
- "30"
15+
Content-Type:
16+
- application/json
17+
User-Agent:
18+
- facturapi-python/0.1.2.dev0
19+
method: POST
20+
uri: https://www.facturapi.io/v2/invoices/67e59a55f4f823d3d978f3cb/email
21+
response:
22+
body:
23+
string: '{"ok":true}'
24+
headers:
25+
Content-Length:
26+
- "11"
27+
access-control-allow-origin:
28+
- "*"
29+
content-security-policy:
30+
- "default-src 'self';font-src https:;img-src data: https:;script-src 'unsafe-inline'
31+
https:;style-src https: 'unsafe-inline';object-src 'none';connect-src
32+
https:;frame-src self *.stripe.com *.facebook.com;upgrade-insecure-requests"
33+
content-type:
34+
- application/json; charset=utf-8
35+
cross-origin-opener-policy:
36+
- same-origin-allow-popups
37+
cross-origin-resource-policy:
38+
- same-site
39+
date:
40+
- Thu, 27 Mar 2025 19:45:43 GMT
41+
etag:
42+
- W/"b-Ai2R8hgEarLmHKwesT1qcY913ys"
43+
origin-agent-cluster:
44+
- ?1
45+
referrer-policy:
46+
- no-referrer
47+
server:
48+
- Google Frontend
49+
strict-transport-security:
50+
- max-age=15552000; includeSubDomains
51+
x-cloud-trace-context:
52+
- a37cf4f44f040b2436c140d1010f163d
53+
x-content-type-options:
54+
- nosniff
55+
x-dns-prefetch-control:
56+
- "off"
57+
x-download-options:
58+
- noopen
59+
x-facturapi-log-id:
60+
- 67e5aae72f593170271effbc
61+
x-frame-options:
62+
- SAMEORIGIN
63+
x-permitted-cross-domain-policies:
64+
- none
65+
x-xss-protection:
66+
- "0"
67+
status:
68+
code: 200
69+
message: OK
70+
version: 1
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
interactions:
2+
- request:
3+
body: "{}"
4+
headers:
5+
Accept:
6+
- "*/*"
7+
Accept-Encoding:
8+
- gzip, deflate
9+
Authorization:
10+
- DUMMY
11+
Connection:
12+
- keep-alive
13+
Content-Length:
14+
- "30"
15+
Content-Type:
16+
- application/json
17+
User-Agent:
18+
- facturapi-python/0.1.2.dev0
19+
method: POST
20+
uri: https://www.facturapi.io/v2/invoices/67e59a55f4f823d3d978f3cb/email
21+
response:
22+
body:
23+
string: '{"ok":true}'
24+
headers:
25+
Content-Length:
26+
- "11"
27+
access-control-allow-origin:
28+
- "*"
29+
content-security-policy:
30+
- "default-src 'self';font-src https:;img-src data: https:;script-src 'unsafe-inline'
31+
https:;style-src https: 'unsafe-inline';object-src 'none';connect-src
32+
https:;frame-src self *.stripe.com *.facebook.com;upgrade-insecure-requests"
33+
content-type:
34+
- application/json; charset=utf-8
35+
cross-origin-opener-policy:
36+
- same-origin-allow-popups
37+
cross-origin-resource-policy:
38+
- same-site
39+
date:
40+
- Thu, 27 Mar 2025 19:45:43 GMT
41+
etag:
42+
- W/"b-Ai2R8hgEarLmHKwesT1qcY913ys"
43+
origin-agent-cluster:
44+
- ?1
45+
referrer-policy:
46+
- no-referrer
47+
server:
48+
- Google Frontend
49+
strict-transport-security:
50+
- max-age=15552000; includeSubDomains
51+
x-cloud-trace-context:
52+
- a37cf4f44f040b2436c140d1010f163d
53+
x-content-type-options:
54+
- nosniff
55+
x-dns-prefetch-control:
56+
- "off"
57+
x-download-options:
58+
- noopen
59+
x-facturapi-log-id:
60+
- 67e5aae72f593170271effbc
61+
x-frame-options:
62+
- SAMEORIGIN
63+
x-permitted-cross-domain-policies:
64+
- none
65+
x-xss-protection:
66+
- "0"
67+
status:
68+
code: 200
69+
message: OK
70+
version: 1

tests/resources/test_invoices.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
from facturapi.resources.customers import CustomerRequest
55
from facturapi.resources.invoices import InvoiceRequest
66
from facturapi.types import FileType, PaymentForm
7-
from facturapi.types.exc import MultipleResultsFound, NoResultFound
7+
from facturapi.types.exc import (
8+
FacturapiResponseException,
9+
MultipleResultsFound,
10+
NoResultFound,
11+
)
812
from facturapi.types.general import CustomerAddress, ItemPart
913

1014

@@ -104,6 +108,40 @@ def test_cancel_invoice():
104108
assert invoice.cancellation_status == 'accepted'
105109

106110

111+
@pytest.mark.vcr
112+
def test_send_invoice_by_email_without_email():
113+
invoice_id = "67e59a55f4f823d3d978f3cb"
114+
email_sent = facturapi.Invoice.send_by_email(invoice_id=invoice_id)
115+
assert email_sent
116+
117+
118+
@pytest.mark.vcr
119+
def test_send_invoice_by_email_with_email():
120+
invoice_id = "67e59a55f4f823d3d978f3cb"
121+
email = "frida_kahlo@test.com"
122+
recipients = [email]
123+
email_sent = facturapi.Invoice.send_by_email(
124+
invoice_id=invoice_id, recipients=recipients
125+
)
126+
assert email_sent
127+
128+
129+
@pytest.mark.vcr
130+
def test_send_invoice_by_email_false():
131+
invoice_id = "INVOICE01"
132+
email_sent = facturapi.Invoice.send_by_email(invoice_id=invoice_id)
133+
assert not email_sent
134+
135+
136+
@pytest.mark.vcr
137+
def test_send_invoice_by_email_invoice_not_found():
138+
invoice_id = "INVOICE_NOT_FOUND"
139+
with pytest.raises(FacturapiResponseException) as exc_info:
140+
facturapi.Invoice.send_by_email(invoice_id=invoice_id)
141+
exc_str = str(exc_info.value)
142+
assert f'Invoice with Id "{invoice_id}" was not found' in exc_str
143+
144+
107145
@pytest.mark.vcr
108146
def test_download_invoice():
109147
invoice_id = 'INVOICE01'

0 commit comments

Comments
 (0)