Projet

Général

Profil

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

Nicolas Roche, 03 juin 2021 09:47

Télécharger (17 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                    | 158 ++++++++++++++----
 5 files changed, 264 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 = 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
-