From b50e9010f53e7a2769ecec2509ac72953cc7dd1c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Corentin=20S=C3=A9chet?= <csechet@entrouvert.com>
Date: Tue, 15 Feb 2022 13:37:27 +0100
Subject: [PATCH] sfr_dmc: implemented SFR DMC sms connector (#20973)

---
 passerelle/apps/sfr_dmc/__init__.py           |   0
 .../apps/sfr_dmc/migrations/0001_initial.py   |  87 ++++++++++
 .../apps/sfr_dmc/migrations/__init__.py       |   0
 passerelle/apps/sfr_dmc/models.py             | 163 ++++++++++++++++++
 passerelle/settings.py                        |   3 +-
 5 files changed, 252 insertions(+), 1 deletion(-)
 create mode 100644 passerelle/apps/sfr_dmc/__init__.py
 create mode 100644 passerelle/apps/sfr_dmc/migrations/0001_initial.py
 create mode 100644 passerelle/apps/sfr_dmc/migrations/__init__.py
 create mode 100644 passerelle/apps/sfr_dmc/models.py

diff --git a/passerelle/apps/sfr_dmc/__init__.py b/passerelle/apps/sfr_dmc/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/passerelle/apps/sfr_dmc/migrations/0001_initial.py b/passerelle/apps/sfr_dmc/migrations/0001_initial.py
new file mode 100644
index 00000000..7b6627b2
--- /dev/null
+++ b/passerelle/apps/sfr_dmc/migrations/0001_initial.py
@@ -0,0 +1,87 @@
+# Generated by Django 2.2.24 on 2022-02-09 11:10
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+import passerelle.sms.models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        ('base', '0029_auto_20210202_1627'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='SfrDmcGateway',
+            fields=[
+                (
+                    'id',
+                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+                ),
+                ('title', models.CharField(max_length=50, verbose_name='Title')),
+                ('slug', models.SlugField(unique=True, verbose_name='Identifier')),
+                ('description', models.TextField(verbose_name='Description')),
+                (
+                    'default_country_code',
+                    models.CharField(default='33', max_length=3, verbose_name='Default country code'),
+                ),
+                (
+                    'default_trunk_prefix',
+                    models.CharField(default='0', max_length=2, verbose_name='Default trunk prefix'),
+                ),
+                (
+                    'max_message_length',
+                    models.IntegerField(
+                        default=2000,
+                        help_text='Messages over this limit will be truncated.',
+                        verbose_name='Maximum message length',
+                    ),
+                ),
+                (
+                    'authorized',
+                    django.contrib.postgres.fields.ArrayField(
+                        base_field=models.CharField(
+                            choices=[
+                                ('fr-metro', 'France mainland (+33 [67])'),
+                                ('fr-domtom', 'France DOM/TOM (+262, etc.)'),
+                                ('be', 'Belgian (+32 4[5-9]) '),
+                                ('all', 'All'),
+                            ],
+                            max_length=32,
+                            null=True,
+                        ),
+                        default=passerelle.sms.models.authorized_default,
+                        size=None,
+                        verbose_name='Authorized Countries',
+                    ),
+                ),
+                (
+                    'allow_premium_rate',
+                    models.BooleanField(
+                        default=False,
+                        help_text='This option is only applyed to France mainland',
+                        verbose_name='Allow premium rate numbers',
+                    ),
+                ),
+                ('service_id', models.CharField(max_length=15, verbose_name='Service ID')),
+                ('service_password', models.CharField(max_length=15, verbose_name='Service Password')),
+                ('space_id', models.IntegerField(verbose_name='Space ID')),
+                (
+                    'users',
+                    models.ManyToManyField(
+                        blank=True,
+                        related_name='_sfrdmcgateway_users_+',
+                        related_query_name='+',
+                        to='base.ApiUser',
+                    ),
+                ),
+            ],
+            options={
+                'verbose_name': 'SFR DMC',
+            },
+        ),
+    ]
diff --git a/passerelle/apps/sfr_dmc/migrations/__init__.py b/passerelle/apps/sfr_dmc/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/passerelle/apps/sfr_dmc/models.py b/passerelle/apps/sfr_dmc/models.py
new file mode 100644
index 00000000..2eca3f27
--- /dev/null
+++ b/passerelle/apps/sfr_dmc/models.py
@@ -0,0 +1,163 @@
+# passerelle - uniform access to multiple data sources and services
+# Copyright (C) 2021  Entr'ouvert
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+import json
+
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+
+from passerelle.sms.models import SMSResource
+from passerelle.utils.jsonresponse import APIError
+
+
+class SfrDmcGateway(SMSResource):
+    service_id = models.CharField(verbose_name=_('Service ID'), max_length=15)
+    service_password = models.CharField(verbose_name=_('Service Password'), max_length=15)
+    space_id = models.IntegerField(verbose_name=_('Space ID'))
+
+    TEST_DEFAULTS = {
+        'create_kwargs': {
+            'service_id': '1234',
+            'service_password': 'krascuky',
+            'space_id': '1234',
+        },
+        'test_vectors': [
+            {
+                'response': '',
+                'result': {
+                    'err': 1,
+                    'err_desc': 'SFR DMC error: some destinations failed',
+                    'data': [
+                        [u'0033688888888', u'SFR DMC error: Bad JSON response'],
+                        [u'0033677777777', u'SFR DMC error: Bad JSON response'],
+                    ],
+                },
+            },
+            {
+                'response': {'success': False, 'no_error_detail_field': 'bad_json'},
+                'result': {
+                    'err': 1,
+                    'err_desc': 'SFR DMC error: some destinations failed',
+                    'data': [
+                        [u'0033688888888', u'SFR DMC error: Bad JSON response'],
+                        [u'0033677777777', u'SFR DMC error: Bad JSON response'],
+                    ],
+                },
+            },
+            {
+                'response': {'success': False, 'errorDetail': 'sfr error description'},
+                'result': {
+                    'err': 1,
+                    'err_desc': 'SFR DMC error: some destinations failed',
+                    'data': [
+                        [
+                            u'0033688888888',
+                            {'result': u'SFR DMC error: sfr error description', 'sms_id': 1234},
+                        ],
+                        [
+                            u'0033677777777',
+                            {'result': u'SFR DMC error: sfr error description', 'sms_id': 1234},
+                        ],
+                    ],
+                },
+            },
+            {
+                'response': {'success': True, 'response': ['1234', '5678']},
+                'result': {
+                    'err': 0,
+                    'data': [
+                        [u'0033688888888', {'result': u'Envoi terminé', 'sms_id': 1234}],
+                        [u'0033677777777', {'result': u'Envoi terminé', 'sms_id': 1234}],
+                    ],
+                },
+            },
+        ],
+    }
+    URL = 'https://www.dmc.sfr-sh.fr/DmcWS/1.5.7'
+
+    class Meta:
+        verbose_name = _('SFR DMC')
+
+    def send_msg(self, text, sender, destinations, **kwargs):
+        """Send a SMS using the SFR DMC provider"""
+        errors = []
+
+        # SFR API limitation, we slice destinations in chunks of 20 items
+        batch_size = 20
+        batches = [destinations[i : i + batch_size] for i in range(0, len(destinations), batch_size)]
+
+        for batch in batches:
+
+            def add_error(message, destination=None):
+                if destination is None:
+                    for dest in batch:
+                        errors.append((dest, f'SFR DMC Error : {message}'))
+                else:
+                    errors.append((destination, f'SFR DMC Error : {message}'))
+
+            local_destinations = []
+            # The API seems to only accept number without country codes.
+            for dest in destinations:
+                if dest[0:4] != '0033':
+                    add_error('SFR DMC API doesn\'t support country code in phone number.')
+                    continue
+                local_destinations.append(f'0{dest[4:]}')
+
+            response = self.requests.get(
+                f'{self.URL}/JsonService/MessagesUnitairesWS/batchSingleCall',
+                params={
+                    'authenticate': json.dumps(
+                        {
+                            'serviceId': self.service_id,
+                            'servicePassword': self.service_password,
+                            'spaceId': self.space_id,
+                        }
+                    ),
+                    'messageUnitaires': json.dumps(
+                        [
+                            {'media': 'SMS', 'textMsg': text, 'from': sender, 'to': dest}
+                            for dest in local_destinations
+                        ]
+                    ),
+                },
+            )
+
+            try:
+                json_data = response.json()
+
+                if 'success' not in json_data:
+                    raise ValueError
+
+                if json_data['success']:
+                    continue
+
+                if not 'errorDetail' in json_data:
+                    raise ValueError
+
+                add_error(json_data['errorDetail'])
+
+            except ValueError:
+                add_error('Bad JSON response')
+
+        if errors:
+            raise APIError('SFR DMC error: some destinations failed', data=errors)
+
+        return None  # credit consumed is unknown
+
+    def get_sms_left(self, type='standard'):
+        raise NotImplementedError
+
+    def get_money_left(self):
+        raise NotImplementedError
diff --git a/passerelle/settings.py b/passerelle/settings.py
index 43ad75e2..0152f331 100644
--- a/passerelle/settings.py
+++ b/passerelle/settings.py
@@ -147,7 +147,6 @@ INSTALLED_APPS = (
     'passerelle.apps.gdc',
     'passerelle.apps.gesbac',
     'passerelle.apps.jsondatastore',
-    'passerelle.apps.sp_fr',
     'passerelle.apps.maelis',
     'passerelle.apps.mdel',
     'passerelle.apps.mdel_ddpacs',
@@ -162,7 +161,9 @@ INSTALLED_APPS = (
     'passerelle.apps.photon',
     'passerelle.apps.plone_restapi',
     'passerelle.apps.sector',
+    'passerelle.apps.sfr_dmc',
     'passerelle.apps.solis',
+    'passerelle.apps.sp_fr',
     'passerelle.apps.twilio',
     'passerelle.apps.vivaticket',
     # backoffice templates and static
-- 
2.34.1

