Last bit of major changes
Closes #1 Closes #5 Closes #6 Closes #8 Closes #9 Closes #10
This commit is contained in:
@@ -35,13 +35,31 @@ class AddressForm(forms.ModelForm):
|
||||
}
|
||||
|
||||
class PaymentMethodForm(forms.ModelForm):
|
||||
card_number = forms.CharField(max_length=19, widget=forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'Card Number'}))
|
||||
cvv = forms.CharField(max_length=4, widget=forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'CVV'}))
|
||||
billing_address = forms.ModelChoiceField(queryset=Address.objects.none(), required=False, empty_label="Select Billing Address")
|
||||
|
||||
class Meta:
|
||||
model = PaymentMethod
|
||||
fields = ('brand', 'last4', 'exp_month', 'exp_year', 'is_default')
|
||||
fields = ('brand', 'exp_month', 'exp_year', 'billing_address', 'is_default')
|
||||
widgets = {
|
||||
'brand': forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'Card Brand (e.g. Visa)'}),
|
||||
'last4': forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'Last 4 Digits', 'maxlength': '4'}),
|
||||
'exp_month': forms.NumberInput(attrs={'class': 'form-input', 'placeholder': 'Exp Month', 'min': '1', 'max': '12'}),
|
||||
'exp_year': forms.NumberInput(attrs={'class': 'form-input', 'placeholder': 'Exp Year', 'min': '2024'}),
|
||||
'billing_address': forms.Select(attrs={'class': 'form-select'}),
|
||||
'is_default': forms.CheckboxInput(attrs={'class': 'form-checkbox'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
user = kwargs.pop('user', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
if user:
|
||||
self.fields['billing_address'].queryset = Address.objects.filter(user=user)
|
||||
|
||||
def save(self, commit=True):
|
||||
pm = super().save(commit=False)
|
||||
pm.card_number = self.cleaned_data['card_number']
|
||||
pm.cvv = self.cleaned_data['cvv']
|
||||
if commit:
|
||||
pm.save()
|
||||
return pm
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 6.0.1 on 2026-01-25 14:03
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='paymentmethod',
|
||||
name='billing_address',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='payment_methods', to='users.address'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='paymentmethod',
|
||||
name='card_number_encrypted',
|
||||
field=models.BinaryField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='paymentmethod',
|
||||
name='cvv_encrypted',
|
||||
field=models.BinaryField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -55,13 +55,44 @@ class Address(models.Model):
|
||||
|
||||
class PaymentMethod(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='payment_methods')
|
||||
# For now, we'll store mock data. In a real app complexity is higher (PCI compliance etc)
|
||||
billing_address = models.ForeignKey(Address, on_delete=models.SET_NULL, null=True, blank=True, related_name='payment_methods')
|
||||
|
||||
brand = models.CharField(max_length=50) # Visa, Mastercard
|
||||
last4 = models.CharField(max_length=4)
|
||||
exp_month = models.PositiveIntegerField()
|
||||
exp_year = models.PositiveIntegerField()
|
||||
is_default = models.BooleanField(default=False)
|
||||
|
||||
# Encrypted fields
|
||||
card_number_encrypted = models.BinaryField(blank=True, null=True)
|
||||
cvv_encrypted = models.BinaryField(blank=True, null=True)
|
||||
|
||||
@property
|
||||
def card_number(self):
|
||||
try:
|
||||
from store.utils import Encryptor
|
||||
return Encryptor.decrypt(self.card_number_encrypted)
|
||||
except ImportError:
|
||||
# Fallback if store isn't ready or circular import
|
||||
return None
|
||||
|
||||
@card_number.setter
|
||||
def card_number(self, value):
|
||||
from store.utils import Encryptor
|
||||
self.card_number_encrypted = Encryptor.encrypt(value)
|
||||
if value and len(str(value)) >= 4:
|
||||
self.last4 = str(value)[-4:]
|
||||
|
||||
@property
|
||||
def cvv(self):
|
||||
from store.utils import Encryptor
|
||||
return Encryptor.decrypt(self.cvv_encrypted)
|
||||
|
||||
@cvv.setter
|
||||
def cvv(self, value):
|
||||
from store.utils import Encryptor
|
||||
self.cvv_encrypted = Encryptor.encrypt(value)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.brand} ending in {self.last4}"
|
||||
|
||||
|
||||
28
users/tests/test_payment.py
Normal file
28
users/tests/test_payment.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from django.test import TestCase
|
||||
from users.models import PaymentMethod, User
|
||||
from store.utils import Encryptor
|
||||
|
||||
class PaymentEncryptionTest(TestCase):
|
||||
def test_encryption_decryption(self):
|
||||
user = User.objects.create_user(username='testuser', password='password')
|
||||
pm = PaymentMethod(user=user, brand='Visa', exp_month=12, exp_year=2030)
|
||||
|
||||
# Test setter
|
||||
pm.card_number = '1234567890123456'
|
||||
pm.cvv = '123'
|
||||
|
||||
# Verify it's encrypted
|
||||
self.assertIsNotNone(pm.card_number_encrypted)
|
||||
self.assertNotEqual(pm.card_number_encrypted, b'1234567890123456')
|
||||
|
||||
# Verify getter
|
||||
self.assertEqual(pm.card_number, '1234567890123456')
|
||||
self.assertEqual(pm.cvv, '123')
|
||||
|
||||
# Save and retrieval
|
||||
pm.save()
|
||||
|
||||
pm_fetched = PaymentMethod.objects.get(id=pm.id)
|
||||
self.assertEqual(pm_fetched.card_number, '1234567890123456')
|
||||
self.assertEqual(pm_fetched.cvv, '123')
|
||||
self.assertEqual(pm_fetched.last4, '3456')
|
||||
@@ -105,7 +105,7 @@ def delete_address_view(request, pk):
|
||||
@login_required
|
||||
def add_payment_method_view(request):
|
||||
if request.method == 'POST':
|
||||
form = PaymentMethodForm(request.POST)
|
||||
form = PaymentMethodForm(request.POST, user=request.user)
|
||||
if form.is_valid():
|
||||
pm = form.save(commit=False)
|
||||
pm.user = request.user
|
||||
@@ -115,7 +115,7 @@ def add_payment_method_view(request):
|
||||
messages.success(request, 'Payment method added successfully.')
|
||||
return redirect('users:profile')
|
||||
else:
|
||||
form = PaymentMethodForm()
|
||||
form = PaymentMethodForm(user=request.user)
|
||||
return render(request, 'users/payment_form.html', {'form': form})
|
||||
|
||||
@login_required
|
||||
|
||||
Reference in New Issue
Block a user