Last bit of major changes

Closes #1
Closes #5
Closes #6
Closes #8
Closes #9
Closes #10
This commit is contained in:
2026-01-26 04:11:38 -06:00
parent 1cd87156bd
commit 739d136209
24 changed files with 1157 additions and 410 deletions

View File

@@ -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

View File

@@ -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),
),
]

View File

@@ -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}"

View 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')

View File

@@ -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