Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Capture Payment Phone numbers #50

Merged
merged 10 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion conftest.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import base64

import pytest
from oauth2_provider.models import Application
from rest_framework.test import APIClient

from users.factories import UserFactory, FCMDeviceFactory

from messaging.factories import ServerFactory

@pytest.fixture
def user(db):
Expand Down Expand Up @@ -32,3 +33,29 @@ def auth_device(user):
client = APIClient()
client.credentials(HTTP_AUTHORIZATION=cred)
return client

@pytest.fixture
def oauth_app(user):
application = Application(
name="Test Application",
redirect_uris="http://localhost",
user=user,
client_type=Application.CLIENT_CONFIDENTIAL,
authorization_grant_type=Application.GRANT_CLIENT_CREDENTIALS,
)
application.raw_client_secret = application.client_secret
application.save()
return application


@pytest.fixture
def server(oauth_app):
return ServerFactory(oauth_application=oauth_app)


@pytest.fixture
def authed_client(client, oauth_app):
auth = f'{oauth_app.client_id}:{oauth_app.raw_client_secret}'.encode('utf-8')
credentials = base64.b64encode(auth).decode('utf-8')
client.defaults['HTTP_AUTHORIZATION'] = 'Basic ' + credentials
return client
1 change: 1 addition & 0 deletions connectid/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
'users.apps.UsersConfig',
'messaging',
'oauth2_provider',
'payments',
'rest_framework',
'axes',
'fcm_django',
Expand Down
51 changes: 13 additions & 38 deletions messaging/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,42 +8,17 @@
import pytest
from django.urls import reverse
from firebase_admin import messaging
from oauth2_provider.models import Application
from rest_framework import status
from rest_framework.test import APITestCase

from messaging.factories import ChannelFactory, MessageFactory, ServerFactory
from messaging.models import Channel, Message, MessageStatus
from messaging.serializers import MessageData
from users.factories import FCMDeviceFactory

APPLICATION_JSON = "application/json"


@pytest.fixture
def oauth_app(user):
application = Application(
name="Test Application",
redirect_uris="http://localhost",
user=user,
client_type=Application.CLIENT_CONFIDENTIAL,
authorization_grant_type=Application.GRANT_CLIENT_CREDENTIALS,
)
application.raw_client_secret = application.client_secret
application.save()
return application
from payments.models import PaymentProfile
from users.factories import FCMDeviceFactory, UserFactory


@pytest.fixture
def server(oauth_app):
return ServerFactory(oauth_application=oauth_app)


@pytest.fixture
def authed_client(client, oauth_app):
auth = f'{oauth_app.client_id}:{oauth_app.raw_client_secret}'.encode('utf-8')
credentials = base64.b64encode(auth).decode('utf-8')
client.defaults['HTTP_AUTHORIZATION'] = 'Basic ' + credentials
return client
APPLICATION_JSON = "application/json"


def test_send_message(authed_client, fcm_device):
Expand All @@ -54,7 +29,7 @@ def test_send_message(authed_client, fcm_device):
"username": fcm_device.user.username,
"body": "test message",
"data": {"test": "data"},
}, content_type="application/json")
}, content_type=APPLICATION_JSON)
assert response.status_code == 200, response.content
assert response.json() == {
'all_success': True,
Expand Down Expand Up @@ -90,7 +65,7 @@ def test_send_message_bulk(authed_client, fcm_device):
"data": {"test": "data2"},
}
]
}, content_type="application/json")
}, content_type=APPLICATION_JSON)

assert response.status_code == 200, response.content
assert mock_send_message.call_count == 2
Expand Down Expand Up @@ -270,7 +245,7 @@ def test_multiple_messages(self, auth_device, channel, server):
response = auth_device.post(
self.url,
data=json.dumps(data),
content_type="application/json",
content_type=APPLICATION_JSON,
)
json_data = response.json()

Expand Down Expand Up @@ -356,7 +331,7 @@ def test_consent(self, auth_device, channel, server, consent=False, ):
}
json_data = json.dumps(data)
response = auth_device.post(
self.url, json_data, content_type="application/json"
self.url, json_data, content_type=APPLICATION_JSON
)

assert response.status_code == status.HTTP_200_OK
Expand All @@ -383,7 +358,7 @@ def test_invalid_channel_id(self, auth_device):
url = reverse("messaging:update_consent")
data = {"channel": str(uuid4()), "consent": False}
data = json.dumps(data)
response = auth_device.post(url, data, content_type="application/json")
response = auth_device.post(url, data, content_type=APPLICATION_JSON)
assert response.status_code == status.HTTP_404_NOT_FOUND


Expand All @@ -397,7 +372,7 @@ def test_update_received(self, auth_device, channel):

data = {"messages": message_ids}
data = json.dumps(data)
response = auth_device.post(self.url, data, content_type="application/json")
response = auth_device.post(self.url, data, content_type=APPLICATION_JSON)

assert response.status_code == status.HTTP_200_OK

Expand All @@ -408,7 +383,7 @@ def test_update_received(self, auth_device, channel):
def test_empty_message_list(self, auth_device):
data = {"messages": []}
data = json.dumps(data)
response = auth_device.post(self.url, data, content_type="application/json")
response = auth_device.post(self.url, data, content_type=APPLICATION_JSON)

assert response.status_code == status.HTTP_400_BAD_REQUEST
assert Message.objects.filter(received__isnull=False).count() == 0
Expand All @@ -417,7 +392,7 @@ def test_invalid_message_ids(self, auth_device):
invalid_message_ids = [str(uuid4()), str(uuid4())]
data = {"messages": invalid_message_ids}
data = json.dumps(data)
response = auth_device.post(self.url, data, content_type="application/json")
response = auth_device.post(self.url, data, content_type=APPLICATION_JSON)

assert response.status_code == status.HTTP_404_NOT_FOUND
assert Message.objects.filter(received__isnull=False).count() == 0
Expand All @@ -433,7 +408,7 @@ def test_grouped_channel_messages(self, mock_send_messages, auth_device):

data = {"messages": message_ids}
data = json.dumps(data)
response = auth_device.post(self.url, data, content_type="application/json")
response = auth_device.post(self.url, data, content_type=APPLICATION_JSON)

assert response.status_code == status.HTTP_200_OK

Expand Down
Empty file added payments/__init__.py
Empty file.
6 changes: 6 additions & 0 deletions payments/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class PaymentsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'payments'
32 changes: 32 additions & 0 deletions payments/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 4.1.7 on 2024-11-09 10:16

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import phonenumber_field.modelfields


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='PaymentProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region=None)),
('owner_name', models.TextField(max_length=150, blank=True)),
('telecom_provider', models.CharField(blank=True, max_length=50, null=True)),
('is_verified', models.BooleanField(default=False)),
('status', models.CharField(choices=[('pending', 'Pending'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=10)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='payment_profile', to=settings.AUTH_USER_MODEL)),
],
),
]
Empty file added payments/migrations/__init__.py
Empty file.
34 changes: 34 additions & 0 deletions payments/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from django.db import models

from phonenumber_field.modelfields import PhoneNumberField
from users.models import ConnectUser


class PaymentProfile(models.Model):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there can only be one of these per user, what is the reason to have a separate model rather than adding these columns to the existing model (and then avoiding the extra lookups and possible exceptions).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are too many fields to add on user directly (telecom_provider, otp-verification status, payment validation status).

PENDING = 'pending'
APPROVED = 'approved'
REJECTED = 'rejected'

STATUS_CHOICES = [
(PENDING, 'Pending'),
(APPROVED, 'Approved'),
(REJECTED, 'Rejected'),
]

user = models.OneToOneField(
ConnectUser,
on_delete=models.CASCADE,
related_name='payment_profile'
)
phone_number = PhoneNumberField()
owner_name = models.TextField(max_length=150, blank=True)
telecom_provider = models.CharField(max_length=50, blank=True, null=True)
# whether the number is verified using OTP
is_verified = models.BooleanField(default=False)
status = models.CharField(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does this column do?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tracks whether this is pending for review or approved or rejected by connect user.

max_length=10,
choices=STATUS_CHOICES,
default=PENDING,
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
95 changes: 95 additions & 0 deletions payments/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import base64
import pytest

from django.urls import reverse
from rest_framework import status

from messaging.tests import APPLICATION_JSON
from payments.models import PaymentProfile
from users.factories import UserFactory


@pytest.mark.parametrize(
"data, expected_status, expected_user1_status, expected_user2_status, result",
[
# Scenario 1: Update both statuses successfully
(
[
{"username": "user1", "phone_number": "12345", "status": "approved"},
{"username": "user2", "phone_number": "67890", "status": "rejected"},
],
status.HTTP_200_OK,
"approved",
"rejected",
{"approved": 1, "rejected": 1, "pending": 0}
),
# Scenario 2: No change in status
(
[
{"username": "user2", "phone_number": "67890", "status": "approved"},
],
status.HTTP_200_OK,
"pending", # Should remain unchanged
"approved", # Should remain unchanged
{"approved": 0, "rejected": 0, "pending": 0}
),
# Scenario 3: Invalid user (user doesn't exist)
(
[
{"username": "nonexistent_user", "phone_number": "00000", "status": "rejected"},
],
status.HTTP_404_NOT_FOUND,
"pending", # No change
"approved", # No change
{}
),
# Scenario 4: Multiple users, one invalid
(
[
{"username": "user1", "phone_number": "12345", "status": "approved"},
{"username": "nonexistent_user", "phone_number": "00000", "status": "rejected"},
],
status.HTTP_404_NOT_FOUND,
"pending", # No change
"approved", # No change
{}
),
]
)
def test_validate_phone_numbers(authed_client, data, expected_status, expected_user1_status, expected_user2_status, result):
user1 = UserFactory(username="user1")
user2 = UserFactory(username="user2")
PaymentProfile.objects.create(user=user1, phone_number="12345", status="pending")
PaymentProfile.objects.create(user=user2, phone_number="67890", status="approved")

url = reverse("validate_payment_phone_numbers")

response = authed_client.post(url, {"updates": data}, content_type=APPLICATION_JSON)

assert response.status_code == expected_status

profile1 = PaymentProfile.objects.get(user=user1)
profile2 = PaymentProfile.objects.get(user=user2)

assert profile1.status == expected_user1_status
assert profile2.status == expected_user2_status
if response.status_code == 200:
assert response.json()["result"] == result


def test_fetch_phone_numbers(authed_client):
user1 = UserFactory(username="user1")
user2 = UserFactory(username="user2")
PaymentProfile.objects.create(user=user1, phone_number="12345", status="pending")
PaymentProfile.objects.create(user=user2, phone_number="67890", status="approved")

url = reverse("fetch_payment_phone_numbers")

response = authed_client.get(url, {"usernames": ["user1", "user2"]})
assert len(response.json()['found_payment_numbers']) == 2

response = authed_client.get(url, {"usernames": ["user1", "user2"], "status": "pending"})
assert len(response.json()['found_payment_numbers']) == 1

response = authed_client.get(url, {"usernames": ["user1"], "status": "approved"})
assert len(response.json()['found_payment_numbers']) == 0
Loading