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 all 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
2 changes: 1 addition & 1 deletion conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from rest_framework.test import APIClient

from users.factories import UserFactory, FCMDeviceFactory

from messaging.factories import ServerFactory

@pytest.fixture
def user(db):
Expand Down
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
24 changes: 13 additions & 11 deletions messaging/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
from django.urls import reverse
from firebase_admin import messaging
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
from payments.models import PaymentProfile
from users.factories import FCMDeviceFactory, UserFactory


APPLICATION_JSON = "application/json"

Expand All @@ -21,7 +24,6 @@
def server(oauth_app):
return ServerFactory(oauth_application=oauth_app)


def test_send_message(authed_client, fcm_device):
url = reverse('messaging:send_message')

Expand All @@ -30,7 +32,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 @@ -66,7 +68,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 @@ -246,7 +248,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 @@ -332,7 +334,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 @@ -359,7 +361,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 @@ -373,7 +375,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 @@ -384,7 +386,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 @@ -393,7 +395,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 @@ -409,7 +411,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", "status": "approved"},
{"username": "user2", "status": "rejected"},
],
status.HTTP_200_OK,
"approved",
"rejected",
{"approved": 1, "rejected": 1, "pending": 0}
),
# Scenario 2: No change in status
(
[
{"username": "user2", "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", "status": "rejected"},
],
status.HTTP_404_NOT_FOUND,
"pending", # No change
"approved", # No change
{}
),
# Scenario 4: Multiple users, one invalid
(
[
{"username": "user1", "status": "approved"},
{"username": "nonexistent_user", "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
Loading