0001-caluire-axel-add-pay_invoice-endpoint-53963.patch
passerelle/contrib/caluire_axel/models.py | ||
---|---|---|
20 | 20 |
from django.core.cache import cache |
21 | 21 |
from django.db import models |
22 | 22 |
from django.http import HttpResponse |
23 | 23 |
from django.utils import dateformat |
24 | 24 |
from django.utils.timezone import now |
25 | 25 |
from django.utils.translation import ugettext_lazy as _ |
26 | 26 | |
27 | 27 |
from passerelle.base.models import BaseResource |
28 |
from passerelle.compat import json_loads |
|
28 | 29 |
from passerelle.contrib.utils import axel |
29 | 30 |
from passerelle.utils.api import endpoint |
30 | 31 |
from passerelle.utils.jsonresponse import APIError |
31 | 32 | |
32 | 33 |
from . import schemas, utils |
33 | 34 | |
34 | 35 | |
35 | 36 |
class CaluireAxel(BaseResource): |
... | ... | |
742 | 743 |
) |
743 | 744 |
if not b64content: |
744 | 745 |
raise APIError('PDF error', err_code='error', http_status=404) |
745 | 746 |
response = HttpResponse(content_type='application/pdf') |
746 | 747 |
response['Content-Disposition'] = 'attachment; filename="%s.pdf"' % invoice_id |
747 | 748 |
response.write(b64content) |
748 | 749 |
return response |
749 | 750 | |
751 |
@endpoint( |
|
752 |
display_category=_('Invoices'), |
|
753 |
display_order=5, |
|
754 |
name='regie', |
|
755 |
methods=['post'], |
|
756 |
perm='can_access', |
|
757 |
pattern=r'^(?P<regie_id>[\w-]+)/invoice/(?P<invoice_id>\w+-\d+)/pay/?$', |
|
758 |
example_pattern='{regie_id}/invoice/{invoice_id}/pay', |
|
759 |
description=_('Notify an invoice as paid'), |
|
760 |
parameters={ |
|
761 |
'regie_id': {'description': _('Regie identifier'), 'example_value': 'ENF'}, |
|
762 |
'invoice_id': {'description': _('Invoice identifier'), 'example_value': 'IDFAM-42'}, |
|
763 |
}, |
|
764 |
post={ |
|
765 |
'request_body': { |
|
766 |
'schema': { |
|
767 |
'application/json': schemas.PAYMENT_SCHEMA, |
|
768 |
} |
|
769 |
} |
|
770 |
}, |
|
771 |
) |
|
772 |
def pay_invoice(self, request, regie_id, invoice_id, **kwargs): |
|
773 |
data = json_loads(request.body) |
|
774 |
family_id, invoice_id = invoice_id.split('-') |
|
775 | ||
776 |
invoice = self.get_invoice(regie_id=regie_id, family_id=family_id, invoice_id=invoice_id) |
|
777 |
if invoice is None: |
|
778 |
raise APIError('Invoice not found', err_code='not-found') |
|
779 | ||
780 |
transaction_amount = 42.00 # invoice['amount'] |
|
781 |
payment_mode_id = data['payment_mode_id'] |
|
782 |
post_data = { |
|
783 |
'IDFACTURE': int(invoice_id), |
|
784 |
'IDENTREGIEENC': regie_id, |
|
785 |
'MONTANT': transaction_amount, |
|
786 |
'IDENTMODEREGLEMENT': payment_mode_id, |
|
787 |
} |
|
788 |
try: |
|
789 |
result = schemas.set_paiement(self, {'PORTAIL': {'SETPAIEMENT': post_data}}) |
|
790 |
except axel.AxelError as e: |
|
791 |
raise APIError( |
|
792 |
'Axel error: %s' % e, |
|
793 |
err_code='error', |
|
794 |
data={'xml_request': e.xml_request, 'xml_response': e.xml_response}, |
|
795 |
) |
|
796 | ||
797 |
code = result.json_response['DATA']['PORTAIL']['SETPAIEMENT']['CODE'] |
|
798 |
if code < 0: |
|
799 |
raise APIError('Wrong pay-invoice status', err_code='pay-invoice-code-error-%s' % code) |
|
800 | ||
801 |
return { |
|
802 |
'created': True, |
|
803 |
'data': { |
|
804 |
'xml_request': result.xml_request, |
|
805 |
'xml_response': result.xml_response, |
|
806 |
}, |
|
807 |
} |
|
808 | ||
750 | 809 | |
751 | 810 |
class Link(models.Model): |
752 | 811 |
resource = models.ForeignKey(CaluireAxel, on_delete=models.CASCADE) |
753 | 812 |
name_id = models.CharField(blank=False, max_length=256) |
754 | 813 |
family_id = models.CharField(blank=False, max_length=128) |
755 | 814 |
person_id = models.CharField(blank=False, max_length=128) |
756 | 815 | |
757 | 816 |
class Meta: |
passerelle/contrib/caluire_axel/schemas.py | ||
---|---|---|
77 | 77 |
get_individu = Operation('GetIndividu') |
78 | 78 |
get_list_ecole = Operation('GetListEcole') |
79 | 79 |
get_list_activites = Operation('GetListActivites') |
80 | 80 |
create_inscription_activite = Operation('CreateInscriptionActivite', data_method='setData') |
81 | 81 |
get_agenda = Operation('GetAgenda') |
82 | 82 |
get_factures_a_payer = Operation('GetFacturesaPayer') |
83 | 83 |
get_list_factures = Operation('GetListFactures') |
84 | 84 |
get_pdf_facture = Operation('GetPdfFacture') |
85 |
set_paiement = Operation('SetPaiement', data_method='setData') |
|
85 | 86 | |
86 | 87 | |
87 | 88 |
LINK_SCHEMA = copy.deepcopy( |
88 | 89 |
find_individus.request_schema['properties']['PORTAIL']['properties']['FINDINDIVIDU'] |
89 | 90 |
) |
90 | 91 |
for key in ['NAISSANCE', 'CODEPOSTAL', 'VILLE', 'TEL', 'MAIL']: |
91 | 92 |
LINK_SCHEMA['properties'].pop(key) |
92 | 93 |
LINK_SCHEMA['required'].remove(key) |
... | ... | |
112 | 113 |
}, |
113 | 114 |
'required': [ |
114 | 115 |
'child_id', |
115 | 116 |
'activity_id', |
116 | 117 |
'registration_start_date', |
117 | 118 |
'registration_end_date', |
118 | 119 |
], |
119 | 120 |
} |
121 | ||
122 |
PAYMENT_SCHEMA = { |
|
123 |
'type': 'object', |
|
124 |
'properties': { |
|
125 |
'payment_mode_id': { |
|
126 |
'type': 'string', |
|
127 |
}, |
|
128 |
}, |
|
129 |
'required': ['payment_mode_id'], |
|
130 |
} |
passerelle/contrib/caluire_axel/xsd/Q_SetPaiement.xsd | ||
---|---|---|
1 |
<?xml version="1.0" encoding="utf-8" ?> |
|
2 |
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:all="urn:AllAxelTypes"> |
|
3 |
|
|
4 |
<xsd:import schemaLocation="./AllAxelTypes.xsd" namespace="urn:AllAxelTypes" /> |
|
5 |
|
|
6 |
<xsd:complexType name="SETPAIEMENTType"> |
|
7 |
<xsd:sequence> |
|
8 |
<xsd:element ref="IDFACTURE"/> |
|
9 |
<xsd:element ref="IDENTREGIEENC"/> |
|
10 |
<xsd:element ref="MONTANT"/> |
|
11 |
<xsd:element ref="IDENTMODEREGLEMENT"/> |
|
12 |
</xsd:sequence> |
|
13 |
</xsd:complexType> |
|
14 |
|
|
15 |
<xsd:complexType name="PORTAILType"> |
|
16 |
<xsd:sequence> |
|
17 |
<xsd:element ref="SETPAIEMENT" /> |
|
18 |
</xsd:sequence> |
|
19 |
</xsd:complexType> |
|
20 |
|
|
21 |
<xsd:simpleType name="CODEType"> |
|
22 |
<xsd:restriction base="xsd:string"> |
|
23 |
<xsd:maxLength value="4" /> |
|
24 |
</xsd:restriction> |
|
25 |
</xsd:simpleType> |
|
26 |
|
|
27 |
<xsd:element name="IDFACTURE" type="xsd:positiveInteger"/> |
|
28 |
<xsd:element name="IDENTREGIEENC" type="all:IDREQUIREDType"/> |
|
29 |
<xsd:element name="MONTANT" type="all:MONTANTType"/> |
|
30 |
<xsd:element name="IDENTMODEREGLEMENT" type="CODEType"/> |
|
31 |
|
|
32 |
<xsd:element name="SETPAIEMENT" type="SETPAIEMENTType"/> |
|
33 |
|
|
34 |
<xsd:element name="PORTAIL" type="PORTAILType"/> |
|
35 |
|
|
36 |
</xsd:schema> |
passerelle/contrib/caluire_axel/xsd/R_SetPaiement.xsd | ||
---|---|---|
1 |
<?xml version="1.0" encoding="utf-8" ?> |
|
2 |
<xsd:schema xmlns:all="urn:AllAxelTypes" xmlns:ind="urn:Individu" xmlns:xsd="http://www.w3.org/2001/XMLSchema" > |
|
3 |
|
|
4 |
<xsd:import schemaLocation="./AllAxelTypes.xsd" namespace="urn:AllAxelTypes" /> |
|
5 |
|
|
6 |
<xsd:redefine schemaLocation="./R_ShemaResultat.xsd"> |
|
7 |
<xsd:simpleType name="TYPEType"> |
|
8 |
<xsd:restriction base="TYPEType"> |
|
9 |
<xsd:enumeration value="SetPaiement" /> |
|
10 |
</xsd:restriction> |
|
11 |
</xsd:simpleType> |
|
12 |
|
|
13 |
<xsd:complexType name="PORTAILType"> |
|
14 |
<xsd:complexContent> |
|
15 |
<xsd:extension base="PORTAILType"> |
|
16 |
<xsd:sequence> |
|
17 |
<xsd:element ref="SETPAIEMENT" minOccurs="0" maxOccurs="1"/> |
|
18 |
</xsd:sequence> |
|
19 |
</xsd:extension> |
|
20 |
</xsd:complexContent> |
|
21 |
</xsd:complexType> |
|
22 |
</xsd:redefine> |
|
23 |
|
|
24 |
<xsd:complexType name="SETPAIEMENTType"> |
|
25 |
<xsd:sequence> |
|
26 |
<xsd:element ref="CODE" /> |
|
27 |
</xsd:sequence> |
|
28 |
</xsd:complexType> |
|
29 |
|
|
30 |
<xsd:element name="CODE" type="xsd:integer"/> |
|
31 |
|
|
32 |
<xsd:element name="SETPAIEMENT" type="SETPAIEMENTType"/> |
|
33 |
|
|
34 |
</xsd:schema> |
tests/test_caluire_axel.py | ||
---|---|---|
12 | 12 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | 13 |
# GNU Affero General Public License for more details. |
14 | 14 |
# |
15 | 15 |
# You should have received a copy of the GNU Affero General Public License |
16 | 16 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
17 | 17 | |
18 | 18 |
import copy |
19 | 19 |
import datetime |
20 |
import decimal |
|
20 | 21 |
import os |
21 | 22 |
import xml.etree.ElementTree as ET |
22 | 23 |
from contextlib import contextmanager |
23 | 24 | |
24 | 25 |
import freezegun |
25 | 26 |
import mock |
26 | 27 |
import pytest |
27 | 28 |
import utils |
28 | 29 |
import xmlschema |
29 | 30 | |
30 | 31 |
from passerelle.contrib.caluire_axel import schemas |
31 | 32 |
from passerelle.contrib.caluire_axel.models import CaluireAxel, Link |
32 | 33 |
from passerelle.contrib.utils.axel import AxelError, json_date_format, xml_date_format |
33 | 34 |
from passerelle.utils.soap import SOAPError |
34 | 35 | |
36 |
XML_RESPONSE_TEMPLATE = '''<?xml version="1.0"?> |
|
37 |
<PORTAILSERVICE> |
|
38 |
<RESULTAT> |
|
39 |
<TYPE>%s</TYPE> |
|
40 |
<STATUS>OK</STATUS> |
|
41 |
<DATE>10/10/2010 10:10:01</DATE> |
|
42 |
<COMMENTAIRES><![CDATA[]]></COMMENTAIRES> |
|
43 |
</RESULTAT> |
|
44 |
<DATA> |
|
45 |
%s |
|
46 |
</DATA> |
|
47 |
</PORTAILSERVICE>''' |
|
48 | ||
35 | 49 | |
36 | 50 |
@pytest.fixture |
37 | 51 |
def resource(db): |
38 | 52 |
return utils.make_resource( |
39 | 53 |
CaluireAxel, slug='test', wsdl_url='http://example.net/AXEL_WS/AxelWS.php?wsdl' |
40 | 54 |
) |
41 | 55 | |
42 | 56 | |
... | ... | |
49 | 63 |
} |
50 | 64 | |
51 | 65 | |
52 | 66 |
@pytest.fixture |
53 | 67 |
def family_data(): |
54 | 68 |
filepath = os.path.join(os.path.dirname(__file__), 'data/caluire_axel/family_info.xml') |
55 | 69 |
with open(filepath) as xml: |
56 | 70 |
content = xml.read() |
57 |
resp = ( |
|
58 |
''' |
|
59 |
<?xml version="1.0"?> |
|
60 |
<PORTAILSERVICE> |
|
61 |
<RESULTAT> |
|
62 |
<TYPE>GetFamilleIndividus</TYPE> |
|
63 |
<STATUS>OK</STATUS> |
|
64 |
<DATE>10/10/2010 10:10:01</DATE> |
|
65 |
<COMMENTAIRES><![CDATA[]]></COMMENTAIRES> |
|
66 |
</RESULTAT> |
|
67 |
<DATA> |
|
68 |
%s |
|
69 |
</DATA> |
|
70 |
</PORTAILSERVICE> |
|
71 |
'''.strip() |
|
72 |
% content |
|
73 |
) |
|
71 |
resp = XML_RESPONSE_TEMPLATE % ('GetFamilleIndividus', content) |
|
74 | 72 |
return schemas.get_famille_individus.response_converter.decode(ET.fromstring(resp))['DATA']['PORTAIL'][ |
75 | 73 |
'GETFAMILLE' |
76 | 74 |
] |
77 | 75 | |
78 | 76 | |
79 | 77 |
@pytest.fixture |
80 | 78 |
def register_activity_params(): |
81 | 79 |
return { |
... | ... | |
84 | 82 |
'registration_start_date': '2020-09-01', |
85 | 83 |
'registration_end_date': '2021-08-31', |
86 | 84 |
} |
87 | 85 | |
88 | 86 | |
89 | 87 |
@contextmanager |
90 | 88 |
def mock_data(content, operation, data_method='getData'): |
91 | 89 |
with mock.patch('passerelle.contrib.caluire_axel.models.CaluireAxel.soap_client') as client: |
92 |
resp = ''' |
|
93 |
<?xml version="1.0"?> |
|
94 |
<PORTAILSERVICE> |
|
95 |
<RESULTAT> |
|
96 |
<TYPE>%s</TYPE> |
|
97 |
<STATUS>OK</STATUS> |
|
98 |
<DATE>10/10/2010 10:10:01</DATE> |
|
99 |
<COMMENTAIRES><![CDATA[]]></COMMENTAIRES> |
|
100 |
</RESULTAT> |
|
101 |
<DATA> |
|
102 |
%s |
|
103 |
</DATA> |
|
104 |
</PORTAILSERVICE> |
|
105 |
'''.strip() % ( |
|
106 |
operation, |
|
107 |
content, |
|
108 |
) |
|
90 |
resp = XML_RESPONSE_TEMPLATE % (operation, content) |
|
109 | 91 |
getattr(client.return_value.service, data_method).return_value = resp |
110 | 92 |
yield |
111 | 93 | |
112 | 94 | |
113 | 95 |
def test_operation_status_error(resource): |
114 | 96 |
resp = ''' |
115 | 97 |
<?xml version="1.0"?> |
116 | 98 |
<PORTAILSERVICE> |
... | ... | |
235 | 217 |
<COMMENTAIRES /> |
236 | 218 |
</RESULTAT> |
237 | 219 |
<DATA> |
238 | 220 |
%s |
239 | 221 |
</DATA> |
240 | 222 |
</PORTAILSERVICE>""" |
241 | 223 |
% content |
242 | 224 |
) |
225 | ||
243 | 226 |
with mock_data(content, 'FindIndividus'): |
244 | 227 |
with mock.patch('passerelle.contrib.utils.axel.AxelSchema.decode') as decode: |
245 | 228 |
decode.side_effect = xmlschema.XMLSchemaValidationError(None, None) |
246 | 229 |
resp = app.post_json('/caluire-axel/test/link?NameID=yyy', params=link_params) |
247 | 230 |
assert resp.json['err_desc'].startswith("Axel error: invalid response") |
248 | 231 |
assert resp.json['err'] == 'error' |
249 | 232 |
assert resp.json['data']['xml_request'] == xml_request |
250 | 233 |
assert resp.json['data']['xml_response'] == xml_response |
... | ... | |
1636 | 1619 |
app.get('/caluire-axel/test/regie/MAREGIE/invoice/XXX-42/pdf?NameID=yyy') |
1637 | 1620 |
assert invoice.call_args_list[0][1]['historical'] is False |
1638 | 1621 | |
1639 | 1622 |
with mock.patch('passerelle.contrib.caluire_axel.models.CaluireAxel.get_invoice') as invoice: |
1640 | 1623 |
invoice.return_value = {'has_pdf': True, 'display_id': '42'} |
1641 | 1624 |
with mock_data(pdf_content, 'GetPdfFacture'): |
1642 | 1625 |
app.get('/caluire-axel/test/regie/MAREGIE/invoice/historical-XXX-42/pdf?NameID=yyy') |
1643 | 1626 |
assert invoice.call_args_list[0][1]['historical'] is True |
1627 | ||
1628 | ||
1629 |
def test_pay_invoice_endpoint_axel_error(app, resource): |
|
1630 |
payload = { |
|
1631 |
'payment_mode_id': '', |
|
1632 |
} |
|
1633 |
Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42') |
|
1634 |
with mock.patch('passerelle.contrib.caluire_axel.schemas.get_factures_a_payer') as operation: |
|
1635 |
operation.side_effect = AxelError('FooBar') |
|
1636 |
resp = app.post_json('/caluire-axel/test/regie/MAREGIE/invoice/XXX-42/pay?NameID=yyy', params=payload) |
|
1637 |
assert resp.json['err_desc'] == "Axel error: FooBar" |
|
1638 |
assert resp.json['err'] == 'error' |
|
1639 | ||
1640 |
filepath = os.path.join(os.path.dirname(__file__), 'data/caluire_axel/invoices.xml') |
|
1641 |
with open(filepath) as xml: |
|
1642 |
content = ( |
|
1643 |
'''<PORTAIL> |
|
1644 |
<GETFACTURESAPAYER> |
|
1645 |
%s |
|
1646 |
</GETFACTURESAPAYER> |
|
1647 |
</PORTAIL>''' |
|
1648 |
% xml.read() |
|
1649 |
) |
|
1650 |
with mock_data(content, 'GetFacturesaPayer'): |
|
1651 |
with mock.patch('passerelle.contrib.caluire_axel.schemas.set_paiement') as operation: |
|
1652 |
operation.side_effect = AxelError('FooBar') |
|
1653 |
resp = app.post_json( |
|
1654 |
'/caluire-axel/test/regie/MAREGIE/invoice/XXX-42/pay?NameID=yyy', params=payload |
|
1655 |
) |
|
1656 |
assert resp.json['err_desc'] == "Axel error: FooBar" |
|
1657 |
assert resp.json['err'] == 'error' |
|
1658 | ||
1659 |
content2 = ''' |
|
1660 |
<PORTAIL> |
|
1661 |
<SETPAIEMENT> |
|
1662 |
<CODE>-3</CODE> |
|
1663 |
</SETPAIEMENT> |
|
1664 |
</PORTAIL>''' |
|
1665 |
with mock.patch('passerelle.contrib.caluire_axel.models.CaluireAxel.soap_client') as client: |
|
1666 |
client.return_value.service.getData.return_value = XML_RESPONSE_TEMPLATE % ( |
|
1667 |
'GetFacturesaPayer', |
|
1668 |
content, |
|
1669 |
) |
|
1670 |
client.return_value.service.setData.return_value = XML_RESPONSE_TEMPLATE % ('SetPaiement', content2) |
|
1671 |
resp = app.post_json('/caluire-axel/test/regie/MAREGIE/invoice/XXX-42/pay?NameID=yyy', params=payload) |
|
1672 |
assert resp.json['err_desc'] == "Wrong pay-invoice status" |
|
1673 |
assert resp.json['err'] == 'pay-invoice-code-error--3' |
|
1674 | ||
1675 | ||
1676 |
def test_pay_invoice_endpoint_bad_request(app, resource): |
|
1677 |
resp = app.post_json( |
|
1678 |
'/caluire-axel/test/regie/MAREGIE/invoice/XXX-42/pay?NameID=yyy', params={}, status=400 |
|
1679 |
) |
|
1680 |
assert resp.json['err_desc'] == "'payment_mode_id' is a required property" |
|
1681 | ||
1682 | ||
1683 |
def test_pay_invoice_endpoint_no_result(app, resource): |
|
1684 |
payload = { |
|
1685 |
'payment_mode_id': '', |
|
1686 |
} |
|
1687 |
filepath = os.path.join(os.path.dirname(__file__), 'data/caluire_axel/invoices.xml') |
|
1688 |
with open(filepath) as xml: |
|
1689 |
content = ( |
|
1690 |
'''<PORTAIL> |
|
1691 |
<GETFACTURESAPAYER> |
|
1692 |
%s |
|
1693 |
</GETFACTURESAPAYER> |
|
1694 |
</PORTAIL>''' |
|
1695 |
% xml.read() |
|
1696 |
) |
|
1697 |
with mock_data(content, 'GetFacturesaPayer'): |
|
1698 |
resp = app.post_json('/caluire-axel/test/regie/MAREGIE/invoice/XXX-35/pay?NameID=yyy', params=payload) |
|
1699 |
assert resp.json['err_desc'] == "Invoice not found" |
|
1700 |
assert resp.json['err'] == 'not-found' |
|
1701 | ||
1702 | ||
1703 |
def test_pay_invoice_endpoint(app, resource): |
|
1704 |
payload = { |
|
1705 |
'payment_mode_id': '', |
|
1706 |
} |
|
1707 |
Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42') |
|
1708 |
filepath = os.path.join(os.path.dirname(__file__), 'data/caluire_axel/invoices.xml') |
|
1709 |
with open(filepath) as xml: |
|
1710 |
content = ( |
|
1711 |
''' |
|
1712 |
<PORTAIL> |
|
1713 |
<GETFACTURESAPAYER> |
|
1714 |
%s |
|
1715 |
</GETFACTURESAPAYER> |
|
1716 |
</PORTAIL>''' |
|
1717 |
% xml.read() |
|
1718 |
) |
|
1719 |
content2 = ''' |
|
1720 |
<PORTAIL> |
|
1721 |
<SETPAIEMENT> |
|
1722 |
<CODE>0</CODE> |
|
1723 |
</SETPAIEMENT> |
|
1724 |
</PORTAIL>''' |
|
1725 |
with mock.patch('passerelle.contrib.caluire_axel.models.CaluireAxel.soap_client') as client: |
|
1726 |
client.return_value.service.getData.return_value = XML_RESPONSE_TEMPLATE % ( |
|
1727 |
'GetFacturesaPayer', |
|
1728 |
content, |
|
1729 |
) |
|
1730 |
client.return_value.service.setData.return_value = XML_RESPONSE_TEMPLATE % ('SetPaiement', content2) |
|
1731 |
resp = app.post_json('/caluire-axel/test/regie/MAREGIE/invoice/XXX-42/pay?NameID=yyy', params=payload) |
|
1732 |
assert resp.json['err'] == 0 |
|
1733 |
assert resp.json['created'] is True |
|
1644 |
- |