Projet

Général

Profil

0001-caluire-axel-add-pay_invoice-endpoint-53963.patch

Nicolas Roche, 02 juin 2021 17:32

Télécharger (17,1 ko)

Voir les différences:

Subject: [PATCH] caluire-axel: add pay_invoice endpoint (#53963)

 passerelle/contrib/caluire_axel/models.py     |  59 +++++++
 passerelle/contrib/caluire_axel/schemas.py    |  11 ++
 .../caluire_axel/xsd/Q_SetPaiement.xsd        |  36 ++++
 .../caluire_axel/xsd/R_SetPaiement.xsd        |  34 ++++
 tests/test_caluire_axel.py                    | 159 ++++++++++++++----
 5 files changed, 265 insertions(+), 34 deletions(-)
 create mode 100644 passerelle/contrib/caluire_axel/xsd/Q_SetPaiement.xsd
 create mode 100644 passerelle/contrib/caluire_axel/xsd/R_SetPaiement.xsd
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

  
35 36

  
37
XML_RESPONSE_TEMPLATE = '''<?xml version="1.0"?>
38
<PORTAILSERVICE>
39
  <RESULTAT>
40
    <TYPE>%s</TYPE>
41
    <STATUS>OK</STATUS>
42
    <DATE>10/10/2010 10:10:01</DATE>
43
    <COMMENTAIRES><![CDATA[]]></COMMENTAIRES>
44
  </RESULTAT>
45
  <DATA>
46
    %s
47
  </DATA>
48
</PORTAILSERVICE>'''
49

  
50

  
36 51
@pytest.fixture
37 52
def resource(db):
38 53
    return utils.make_resource(
39 54
        CaluireAxel, slug='test', wsdl_url='http://example.net/AXEL_WS/AxelWS.php?wsdl'
40 55
    )
41 56

  
42 57

  
43 58
@pytest.fixture
......
49 64
    }
50 65

  
51 66

  
52 67
@pytest.fixture
53 68
def family_data():
54 69
    filepath = os.path.join(os.path.dirname(__file__), 'data/caluire_axel/family_info.xml')
55 70
    with open(filepath) as xml:
56 71
        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
    )
72
    resp = XML_RESPONSE_TEMPLATE % ('GetFamilleIndividus', content)
74 73
    return schemas.get_famille_individus.response_converter.decode(ET.fromstring(resp))['DATA']['PORTAIL'][
75 74
        'GETFAMILLE'
76 75
    ]
77 76

  
78 77

  
79 78
@pytest.fixture
80 79
def register_activity_params():
81 80
    return {
......
84 83
        'registration_start_date': '2020-09-01',
85 84
        'registration_end_date': '2021-08-31',
86 85
    }
87 86

  
88 87

  
89 88
@contextmanager
90 89
def mock_data(content, operation, data_method='getData'):
91 90
    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
        )
91
        resp = XML_RESPONSE_TEMPLATE % (operation, content)
109 92
        getattr(client.return_value.service, data_method).return_value = resp
110 93
        yield
111 94

  
112 95

  
113 96
def test_operation_status_error(resource):
114 97
    resp = '''
115 98
    <?xml version="1.0"?>
116 99
    <PORTAILSERVICE>
......
235 218
    <COMMENTAIRES />
236 219
  </RESULTAT>
237 220
  <DATA>
238 221
    %s
239 222
  </DATA>
240 223
</PORTAILSERVICE>"""
241 224
        % content
242 225
    )
226

  
243 227
    with mock_data(content, 'FindIndividus'):
244 228
        with mock.patch('passerelle.contrib.utils.axel.AxelSchema.decode') as decode:
245 229
            decode.side_effect = xmlschema.XMLSchemaValidationError(None, None)
246 230
            resp = app.post_json('/caluire-axel/test/link?NameID=yyy', params=link_params)
247 231
    assert resp.json['err_desc'].startswith("Axel error: invalid response")
248 232
    assert resp.json['err'] == 'error'
249 233
    assert resp.json['data']['xml_request'] == xml_request
250 234
    assert resp.json['data']['xml_response'] == xml_response
......
1636 1620
            app.get('/caluire-axel/test/regie/MAREGIE/invoice/XXX-42/pdf?NameID=yyy')
1637 1621
    assert invoice.call_args_list[0][1]['historical'] is False
1638 1622

  
1639 1623
    with mock.patch('passerelle.contrib.caluire_axel.models.CaluireAxel.get_invoice') as invoice:
1640 1624
        invoice.return_value = {'has_pdf': True, 'display_id': '42'}
1641 1625
        with mock_data(pdf_content, 'GetPdfFacture'):
1642 1626
            app.get('/caluire-axel/test/regie/MAREGIE/invoice/historical-XXX-42/pdf?NameID=yyy')
1643 1627
    assert invoice.call_args_list[0][1]['historical'] is True
1628

  
1629

  
1630
def test_pay_invoice_endpoint_axel_error(app, resource):
1631
    payload = {
1632
        'payment_mode_id': '',
1633
    }
1634
    Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42')
1635
    with mock.patch('passerelle.contrib.caluire_axel.schemas.get_factures_a_payer') as operation:
1636
        operation.side_effect = AxelError('FooBar')
1637
        resp = app.post_json('/caluire-axel/test/regie/MAREGIE/invoice/XXX-42/pay?NameID=yyy', params=payload)
1638
    assert resp.json['err_desc'] == "Axel error: FooBar"
1639
    assert resp.json['err'] == 'error'
1640

  
1641
    filepath = os.path.join(os.path.dirname(__file__), 'data/caluire_axel/invoices.xml')
1642
    with open(filepath) as xml:
1643
        content = (
1644
            '''<PORTAIL>
1645
  <GETFACTURESAPAYER>
1646
    %s
1647
  </GETFACTURESAPAYER>
1648
</PORTAIL>'''
1649
            % xml.read()
1650
        )
1651
    with mock_data(content, 'GetFacturesaPayer'):
1652
        with mock.patch('passerelle.contrib.caluire_axel.schemas.set_paiement') as operation:
1653
            operation.side_effect = AxelError('FooBar')
1654
            resp = app.post_json(
1655
                '/caluire-axel/test/regie/MAREGIE/invoice/XXX-42/pay?NameID=yyy', params=payload
1656
            )
1657
    assert resp.json['err_desc'] == "Axel error: FooBar"
1658
    assert resp.json['err'] == 'error'
1659

  
1660
    content2 = '''
1661
<PORTAIL>
1662
  <SETPAIEMENT>
1663
    <CODE>-3</CODE>
1664
  </SETPAIEMENT>
1665
</PORTAIL>'''
1666
    with mock.patch('passerelle.contrib.caluire_axel.models.CaluireAxel.soap_client') as client:
1667
        client.return_value.service.getData.return_value = XML_RESPONSE_TEMPLATE % (
1668
            'GetFacturesaPayer',
1669
            content,
1670
        )
1671
        client.return_value.service.setData.return_value = XML_RESPONSE_TEMPLATE % ('SetPaiement', content2)
1672
        resp = app.post_json('/caluire-axel/test/regie/MAREGIE/invoice/XXX-42/pay?NameID=yyy', params=payload)
1673
    assert resp.json['err_desc'] == "Wrong pay-invoice status"
1674
    assert resp.json['err'] == 'pay-invoice-code-error--3'
1675

  
1676

  
1677
def test_pay_invoice_endpoint_bad_request(app, resource):
1678
    resp = app.post_json(
1679
        '/caluire-axel/test/regie/MAREGIE/invoice/XXX-42/pay?NameID=yyy', params={}, status=400
1680
    )
1681
    assert resp.json['err_desc'] == "'payment_mode_id' is a required property"
1682

  
1683

  
1684
def test_pay_invoice_endpoint_no_result(app, resource):
1685
    payload = {
1686
        'payment_mode_id': '',
1687
    }
1688
    filepath = os.path.join(os.path.dirname(__file__), 'data/caluire_axel/invoices.xml')
1689
    with open(filepath) as xml:
1690
        content = (
1691
            '''<PORTAIL>
1692
  <GETFACTURESAPAYER>
1693
    %s
1694
  </GETFACTURESAPAYER>
1695
</PORTAIL>'''
1696
            % xml.read()
1697
        )
1698
    with mock_data(content, 'GetFacturesaPayer'):
1699
        resp = app.post_json('/caluire-axel/test/regie/MAREGIE/invoice/XXX-35/pay?NameID=yyy', params=payload)
1700
    assert resp.json['err_desc'] == "Invoice not found"
1701
    assert resp.json['err'] == 'not-found'
1702

  
1703

  
1704
def test_pay_invoice_endpoint(app, resource):
1705
    payload = {
1706
        'payment_mode_id': '',
1707
    }
1708
    Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42')
1709
    filepath = os.path.join(os.path.dirname(__file__), 'data/caluire_axel/invoices.xml')
1710
    with open(filepath) as xml:
1711
        content = (
1712
            '''
1713
<PORTAIL>
1714
  <GETFACTURESAPAYER>
1715
    %s
1716
  </GETFACTURESAPAYER>
1717
</PORTAIL>'''
1718
            % xml.read()
1719
        )
1720
    content2 = '''
1721
<PORTAIL>
1722
  <SETPAIEMENT>
1723
    <CODE>0</CODE>
1724
  </SETPAIEMENT>
1725
</PORTAIL>'''
1726
    with mock.patch('passerelle.contrib.caluire_axel.models.CaluireAxel.soap_client') as client:
1727
        client.return_value.service.getData.return_value = XML_RESPONSE_TEMPLATE % (
1728
            'GetFacturesaPayer',
1729
            content,
1730
        )
1731
        client.return_value.service.setData.return_value = XML_RESPONSE_TEMPLATE % ('SetPaiement', content2)
1732
        resp = app.post_json('/caluire-axel/test/regie/MAREGIE/invoice/XXX-42/pay?NameID=yyy', params=payload)
1733
    assert resp.json['err'] == 0
1734
    assert resp.json['created'] is True
1644
-