basket: make credit payments only when invoice is really paid (#85794)
gitea/lingo/pipeline/head This commit looks good Details

This commit is contained in:
Lauréline Guérin 2024-01-18 15:15:20 +01:00
parent 6aa201e461
commit a1837bae53
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
13 changed files with 371 additions and 160 deletions

View File

@ -31,8 +31,8 @@ from lingo.invoicing.models import (
CreditAssignment,
DraftInvoice,
Invoice,
InvoiceLinePayment,
Payment,
PaymentType,
Regie,
)
from lingo.utils.requests_wrapper import requests as requests_wrapper
@ -82,6 +82,7 @@ class Basket(models.Model):
basket = cls.objects.filter(invoice=invoice).first()
if not basket:
return
basket.make_payments_with_credits()
basket.status = 'completed'
basket.completed_at = now()
basket.save()
@ -92,12 +93,7 @@ class Basket(models.Model):
line.notify(notification_type)
def revert_assignments(self):
payments = list(
InvoiceLinePayment.objects.filter(line__invoice=self.invoice).values_list('payment', flat=True)
)
InvoiceLinePayment.objects.filter(line__invoice=self.invoice).delete()
CreditAssignment.objects.filter(payment__in=payments).delete()
Payment.objects.filter(pk__in=payments).delete()
CreditAssignment.objects.filter(invoice=self.invoice).delete()
def cancel(self):
self.notify('cancel')
@ -137,20 +133,17 @@ class Basket(models.Model):
def is_expired(self):
return self.status == 'expired' or self.expiry_at <= now()
@cached_property
@property
def total_amount(self):
if self.invoice is not None:
return self.invoice.total_amount
return self.draft_invoice.total_amount
@cached_property
@property
def credit_amount(self):
if self.invoice is not None:
payment_line_qs = InvoiceLinePayment.objects.filter(
line__invoice=self.invoice,
payment__payment_type__slug='credit',
)
return -sum(pl.amount for pl in payment_line_qs)
assignment_qs = CreditAssignment.objects.filter(invoice=self.invoice)
return -sum(a.amount for a in assignment_qs)
if self.total_amount < 0:
return 0
@ -160,7 +153,7 @@ class Basket(models.Model):
available_credit = sum(c.remaining_amount for c in credit_qs)
return -min(self.total_amount, available_credit)
@cached_property
@property
def remaining_amount(self):
return self.total_amount + self.credit_amount
@ -169,6 +162,26 @@ class Basket(models.Model):
regie=self.regie,
basket=self,
)
assignment_qs = CreditAssignment.objects.filter(invoice=self.invoice)
assigned_amount = sum(a.amount for a in assignment_qs)
if assigned_amount == self.invoice.remaining_amount:
# invoice totally paid with credits, make payment
self.make_payments_with_credits()
def make_payments_with_credits(self):
assignment_qs = CreditAssignment.objects.filter(invoice=self.invoice)
assigned_amount = sum(a.amount for a in assignment_qs)
if assigned_amount > 0:
payment_type, dummy = PaymentType.objects.get_or_create(
regie=self.regie, slug='credit', defaults={'label': _('Credit')}
)
payment = Payment.make_payment(
regie=self.regie,
amount=assigned_amount,
payment_type=payment_type,
invoices=[self.invoice],
)
CreditAssignment.objects.filter(invoice=self.invoice).update(payment=payment)
class BasketLine(models.Model):

View File

@ -86,20 +86,26 @@ class BasketValidateView(LoginRequiredMixin, FormView):
# do nothing
return redirect(reverse('lingo-basket-detail'))
if self.basket.draft_invoice.total_amount >= 0:
if self.basket.total_amount >= 0:
self.generate_invoice()
return pay_invoice(request, self.basket.regie, self.basket.invoice)
else:
self.generate_credit()
if self.basket.remaining_amount > 0:
return pay_invoice(
request=request,
regie=self.basket.regie,
invoice=self.basket.invoice,
amount=self.basket.remaining_amount,
)
return redirect(reverse('lingo-basket-detail'))
self.generate_credit()
return redirect(reverse('lingo-basket-detail'))
def generate_invoice(self):
self.basket.invoice = self.basket.draft_invoice.promote()
self.basket.invoice.refresh_from_db() # refresh amounts
self.basket.assign_credits()
self.basket.invoice.refresh_from_db() # refresh amounts
if self.basket.invoice.remaining_amount == 0:
if self.basket.remaining_amount == 0:
self.basket.status = 'completed'
self.basket.validated_at = now()
self.basket.paid_at = now()

View File

@ -125,12 +125,14 @@ def pay_demo(request):
return pay(request, transaction=transaction)
def pay_invoice(request, regie, invoice):
def pay_invoice(request, regie, invoice, amount):
if amount <= 0:
return HttpResponseBadRequest('negative or null amount')
backend = get_object_or_404(PaymentBackend, regie=regie)
transaction = Transaction.objects.create(
status=0,
invoice=invoice,
amount=invoice.total_amount,
amount=amount,
backend=backend,
next_url=reverse_lazy('lingo-basket-detail'),
)

View File

@ -0,0 +1,18 @@
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('invoicing', '0076_line_new_fields'),
]
operations = [
migrations.AddField(
model_name='creditassignment',
name='invoice',
field=models.ForeignKey(
null=True, on_delete=django.db.models.deletion.PROTECT, to='invoicing.invoice'
),
),
]

View File

@ -0,0 +1,19 @@
from django.db import migrations
def forward(apps, schema_editor):
CreditAssignment = apps.get_model('invoicing', 'CreditAssignment')
InvoiceLinePayment = apps.get_model('invoicing', 'InvoiceLinePayment')
for ca in CreditAssignment.objects.all():
ca.invoice = InvoiceLinePayment.objects.filter(payment=ca.payment).first().line.invoice
ca.save()
class Migration(migrations.Migration):
dependencies = [
('invoicing', '0077_credit_assignment'),
]
operations = [
migrations.RunPython(forward, reverse_code=migrations.RunPython.noop),
]

View File

@ -0,0 +1,23 @@
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('invoicing', '0078_credit_assignment'),
]
operations = [
migrations.AlterField(
model_name='creditassignment',
name='invoice',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='invoicing.invoice'),
),
migrations.AlterField(
model_name='creditassignment',
name='payment',
field=models.ForeignKey(
null=True, on_delete=django.db.models.deletion.PROTECT, to='invoicing.payment'
),
),
]

View File

@ -1418,16 +1418,14 @@ class InvoicePayment:
class CreditAssignment(models.Model):
payment = models.ForeignKey(Payment, on_delete=models.PROTECT)
invoice = models.ForeignKey(Invoice, on_delete=models.PROTECT)
payment = models.ForeignKey(Payment, on_delete=models.PROTECT, null=True)
credit = models.ForeignKey(Credit, on_delete=models.PROTECT)
amount = models.DecimalField(max_digits=9, decimal_places=2)
created_at = models.DateTimeField(auto_now_add=True)
@classmethod
def make_assignments(cls, regie, basket):
payment_type, dummy = PaymentType.objects.get_or_create(
regie=regie, slug='credit', defaults={'label': _('Credit')}
)
credit_qs = (
Credit.objects.select_for_update()
.filter(remaining_amount__gt=0, regie=regie, payer_external_id=basket.payer_external_id)
@ -1438,18 +1436,11 @@ class CreditAssignment(models.Model):
amount_to_assign = min(available_credit, basket.invoice.remaining_amount)
if amount_to_assign == 0:
return
# make a payment 'credit'
payment = Payment.make_payment(
regie=regie,
amount=amount_to_assign,
payment_type=payment_type,
invoices=[basket.invoice],
)
# and assign credits
# assign credits
for credit in credit_qs:
paid_amount = decimal.Decimal(min(credit.remaining_amount, amount_to_assign))
CreditAssignment.objects.create(
payment=payment,
invoice=basket.invoice,
credit=credit,
amount=paid_amount,
)

View File

@ -3084,6 +3084,14 @@ def test_list_credits(mock_payer, app, user):
PaymentType.create_defaults(regie)
app.get('/api/regie/foo/credits/', status=404)
invoice = Invoice.objects.create(
label='My invoice',
date_publication=datetime.date(2022, 10, 1),
date_payment_deadline=datetime.date.today(),
date_due=datetime.date(2022, 10, 31),
regie=regie,
payer_external_id='payer:1',
)
payment = Payment.objects.create(
regie=regie,
amount=42,
@ -3113,6 +3121,7 @@ def test_list_credits(mock_payer, app, user):
user_last_name='Name1',
)
CreditAssignment.objects.create(
invoice=invoice,
payment=payment,
credit=credit,
amount=1,
@ -3162,6 +3171,7 @@ def test_list_credits(mock_payer, app, user):
mock_payer.return_value = 'payer:1'
mock_payer.side_effect = None
CreditAssignment.objects.create(
invoice=invoice,
payment=payment,
credit=credit,
amount=41,
@ -3185,6 +3195,14 @@ def test_list_credits_for_payer(app, user):
assert resp.json['err'] == 0
assert resp.json['data'] == []
invoice = Invoice.objects.create(
label='My invoice',
date_publication=datetime.date(2022, 10, 1),
date_payment_deadline=datetime.date.today(),
date_due=datetime.date(2022, 10, 31),
regie=regie,
payer_external_id='payer:1',
)
payment = Payment.objects.create(
regie=regie,
amount=42,
@ -3214,6 +3232,7 @@ def test_list_credits_for_payer(app, user):
user_last_name='Name1',
)
CreditAssignment.objects.create(
invoice=invoice,
payment=payment,
credit=credit,
amount=1,
@ -3249,6 +3268,7 @@ def test_list_credits_for_payer(app, user):
credit.regie = regie
credit.save()
CreditAssignment.objects.create(
invoice=invoice,
payment=payment,
credit=credit,
amount=41,
@ -3269,6 +3289,14 @@ def test_list_history_credits(mock_payer, app, user):
PaymentType.create_defaults(regie)
app.get('/api/regie/foo/credits/history/', status=404)
invoice = Invoice.objects.create(
label='My invoice',
date_publication=datetime.date(2022, 10, 1),
date_payment_deadline=datetime.date.today(),
date_due=datetime.date(2022, 10, 31),
regie=regie,
payer_external_id='payer:1',
)
payment = Payment.objects.create(
regie=regie,
amount=42,
@ -3298,6 +3326,7 @@ def test_list_history_credits(mock_payer, app, user):
user_last_name='Name1',
)
CreditAssignment.objects.create(
invoice=invoice,
payment=payment,
credit=credit,
amount=1,
@ -3312,6 +3341,7 @@ def test_list_history_credits(mock_payer, app, user):
# credit fully assigned
mock_payer.reset_mock()
CreditAssignment.objects.create(
invoice=invoice,
payment=payment,
credit=credit,
amount=41,
@ -3369,6 +3399,14 @@ def test_list_history_credits_for_payer(app, user):
assert resp.json['err'] == 0
assert resp.json['data'] == []
invoice = Invoice.objects.create(
label='My invoice',
date_publication=datetime.date(2022, 10, 1),
date_payment_deadline=datetime.date.today(),
date_due=datetime.date(2022, 10, 31),
regie=regie,
payer_external_id='payer:1',
)
payment = Payment.objects.create(
regie=regie,
amount=42,
@ -3398,6 +3436,7 @@ def test_list_history_credits_for_payer(app, user):
user_last_name='Name1',
)
CreditAssignment.objects.create(
invoice=invoice,
payment=payment,
credit=credit,
amount=1,
@ -3410,6 +3449,7 @@ def test_list_history_credits_for_payer(app, user):
# credit fully assigned
CreditAssignment.objects.create(
invoice=invoice,
payment=payment,
credit=credit,
amount=41,

View File

@ -289,15 +289,19 @@ def test_basket_invoice_pdf(app, simple_user):
# invoice with credit and assignment
basket.status = 'tobepaid'
basket.save()
PaymentType.create_defaults(regie)
payment = Payment.objects.create(
credit = Credit.objects.create(
regie=regie,
amount=10,
payment_type=PaymentType.objects.get(regie=regie, slug='credit'),
payer_external_id='payer:1',
)
InvoiceLinePayment.objects.create(
payment=payment,
line=basket.invoice.lines.first(),
CreditLine.objects.create(
credit=credit,
event_date=datetime.date(2022, 9, 1),
quantity=10,
unit_amount=1,
)
CreditAssignment.objects.create(
invoice=basket.invoice,
credit=credit,
amount=1,
)
resp = app.get('/basket/invoice/pdf/?html', status=200)
@ -443,7 +447,7 @@ def test_basket_validate_generate_invoice(app, simple_user):
resp = app.get('/basket/validate/')
with mock.patch('lingo.basket.views.pay_invoice') as pay_invoice:
pay_invoice.side_effect = lambda *args: redirect(reverse('lingo-basket-detail'))
pay_invoice.side_effect = lambda *args, **kwargs: redirect(reverse('lingo-basket-detail'))
resp = resp.form.submit()
assert resp.location.endswith('/basket/')
basket.refresh_from_db()
@ -501,7 +505,7 @@ def test_basket_validate_generate_invoice(app, simple_user):
basket.save()
resp = app.get('/basket/validate/')
with mock.patch('lingo.basket.views.pay_invoice') as pay_invoice:
pay_invoice.side_effect = lambda *args: redirect(reverse('lingo-basket-detail'))
pay_invoice.side_effect = lambda *args, **kwargs: redirect(reverse('lingo-basket-detail'))
resp = resp.form.submit()
assert resp.location.endswith('/basket/')
basket.refresh_from_db()
@ -512,30 +516,25 @@ def test_basket_validate_generate_invoice(app, simple_user):
invoice = Invoice.objects.latest('pk')
assert basket.invoice == invoice
assert invoice.total_amount == 10
assert invoice.paid_amount == 4
assert invoice.remaining_amount == 6
assert invoice.paid_amount == 0
assert invoice.remaining_amount == 10
credit1.refresh_from_db()
assert credit1.remaining_amount == 0
assert credit1.assigned_amount == 1
credit2.refresh_from_db()
assert credit2.remaining_amount == 0
assert credit2.assigned_amount == 3
assert Payment.objects.count() == 1
payment = Payment.objects.latest('pk')
assert payment.amount == 4
assert payment.payment_type.slug == 'credit'
assert Payment.objects.count() == 0
assert CreditAssignment.objects.count() == 2
assignment1, assignment2 = CreditAssignment.objects.all().order_by('pk')
assert assignment1.amount == 1
assert assignment1.payment == payment
assert assignment1.invoice == invoice
assert assignment1.payment is None
assert assignment1.credit == credit1
assert assignment2.amount == 3
assert assignment2.payment == payment
assert assignment2.invoice == invoice
assert assignment2.payment is None
assert assignment2.credit == credit2
assert payment.invoicelinepayment_set.count() == 1
invoicelinepayment = InvoiceLinePayment.objects.latest('pk')
assert invoicelinepayment.line == invoice.lines.get()
assert invoicelinepayment.amount == 4
def test_basket_validate_generate_invoice_nothing_to_pay(app, simple_user):
@ -583,10 +582,7 @@ def test_basket_validate_generate_invoice_nothing_to_pay(app, simple_user):
)
resp = app.get('/basket/validate/')
with mock.patch('lingo.basket.views.pay_invoice') as pay_invoice, mock.patch(
'lingo.utils.requests_wrapper.RequestsSession.send'
) as mock_send:
pay_invoice.side_effect = lambda *args: redirect(reverse('lingo-basket-detail'))
with mock.patch('lingo.utils.requests_wrapper.RequestsSession.send') as mock_send:
resp = resp.form.submit()
assert {x[0][0].url for x in mock_send.call_args_list} == set()
assert resp.location.endswith('/basket/')
@ -599,6 +595,7 @@ def test_basket_validate_generate_invoice_nothing_to_pay(app, simple_user):
assert basket.invoice == invoice
assert invoice.total_amount == 0
assert Credit.objects.count() == 0
assert Payment.objects.count() == 0
# total is zero, but with credits
invoice_line2.delete()
@ -622,10 +619,7 @@ def test_basket_validate_generate_invoice_nothing_to_pay(app, simple_user):
basket.save()
resp = app.get('/basket/validate/')
with mock.patch('lingo.basket.views.pay_invoice') as pay_invoice, mock.patch(
'lingo.utils.requests_wrapper.RequestsSession.send'
) as mock_send:
pay_invoice.side_effect = lambda *args: redirect(reverse('lingo-basket-detail'))
with mock.patch('lingo.utils.requests_wrapper.RequestsSession.send') as mock_send:
resp = resp.form.submit()
assert {x[0][0].url for x in mock_send.call_args_list} == {
'http://validation1.com/',
@ -653,6 +647,7 @@ def test_basket_validate_generate_invoice_nothing_to_pay(app, simple_user):
assert CreditAssignment.objects.count() == 1
assignment = CreditAssignment.objects.latest('pk')
assert assignment.amount == 1
assert assignment.invoice == invoice
assert assignment.payment == payment
assert assignment.credit == credit
assert payment.invoicelinepayment_set.count() == 1
@ -797,7 +792,7 @@ def test_basket_cancel(app, simple_user):
date_payment_deadline=datetime.date(2023, 4, 22),
date_due=datetime.date(2023, 4, 23),
)
invoice_line = InvoiceLine.objects.create(
InvoiceLine.objects.create(
slug='event-a-foo-bar',
label='Event A',
event_date=datetime.date(2022, 9, 1),
@ -815,11 +810,6 @@ def test_basket_cancel(app, simple_user):
)
app.get('/basket/cancel/', status=404)
# the invoice is partially paid with a credit
payment = Payment.objects.create(
regie=regie,
amount=1,
payment_type=PaymentType.objects.get(regie=regie, slug='credit'),
)
credit = Credit.objects.create(
regie=regie,
payer_external_id='payer:1',
@ -831,15 +821,10 @@ def test_basket_cancel(app, simple_user):
unit_amount=1,
)
CreditAssignment.objects.create(
payment=payment,
invoice=invoice,
credit=credit,
amount=1,
)
InvoiceLinePayment.objects.create(
payment=payment,
line=invoice_line,
amount=1,
)
# a not closed line
line = BasketLine.objects.create(
@ -858,8 +843,6 @@ def test_basket_cancel(app, simple_user):
# good payer_nameid
assert CreditAssignment.objects.count() == 1
assert Payment.objects.count() == 1
assert InvoiceLinePayment.objects.count() == 1
basket.payer_nameid = 'ab' * 16
basket.save()
resp = app.get('/basket/cancel/')
@ -873,8 +856,6 @@ def test_basket_cancel(app, simple_user):
invoice.refresh_from_db()
assert invoice.cancelled_at is not None
assert CreditAssignment.objects.count() == 0
assert Payment.objects.count() == 0
assert InvoiceLinePayment.objects.count() == 0
basket.status = 'tobepaid'
basket.cancelled_at = None

View File

@ -13,9 +13,6 @@ from lingo.invoicing.models import (
DraftInvoiceLine,
Invoice,
InvoiceLine,
InvoiceLinePayment,
Payment,
PaymentType,
Regie,
)
@ -24,7 +21,6 @@ pytestmark = pytest.mark.django_db
def test_basket_expiration():
regie = Regie.objects.create(label='Foo')
PaymentType.create_defaults(regie)
draft_invoice = DraftInvoice.objects.create(
regie=regie,
date_publication=datetime.date(2023, 4, 21),
@ -37,7 +33,7 @@ def test_basket_expiration():
date_payment_deadline=datetime.date(2023, 4, 22),
date_due=datetime.date(2023, 4, 23),
)
invoice_line = InvoiceLine.objects.create(
InvoiceLine.objects.create(
slug='event-a-foo-bar',
label='Event A',
event_date=datetime.date(2022, 9, 1),
@ -58,11 +54,6 @@ def test_basket_expiration():
user_external_id='user:1',
)
# the invoice is partially paid with a credit
payment = Payment.objects.create(
regie=regie,
amount=1,
payment_type=PaymentType.objects.get(regie=regie, slug='credit'),
)
credit = Credit.objects.create(
regie=regie,
payer_external_id='payer:1',
@ -74,15 +65,10 @@ def test_basket_expiration():
unit_amount=1,
)
CreditAssignment.objects.create(
payment=payment,
invoice=invoice,
credit=credit,
amount=1,
)
InvoiceLinePayment.objects.create(
payment=payment,
line=invoice_line,
amount=1,
)
# other credit used on another invoice
draft_invoice2 = DraftInvoice.objects.create(
regie=regie,
@ -96,7 +82,7 @@ def test_basket_expiration():
date_payment_deadline=datetime.date(2023, 4, 22),
date_due=datetime.date(2023, 4, 23),
)
invoice_line2 = InvoiceLine.objects.create(
InvoiceLine.objects.create(
slug='event-a-foo-bar',
label='Event A',
event_date=datetime.date(2022, 9, 1),
@ -111,11 +97,6 @@ def test_basket_expiration():
invoice=invoice2,
expiry_at=now() + datetime.timedelta(minutes=1),
)
payment2 = Payment.objects.create(
regie=regie,
amount=1,
payment_type=PaymentType.objects.get(regie=regie, slug='credit'),
)
credit2 = Credit.objects.create(
regie=regie,
payer_external_id='payer:1',
@ -127,15 +108,10 @@ def test_basket_expiration():
unit_amount=1,
)
CreditAssignment.objects.create(
payment=payment2,
invoice=invoice2,
credit=credit2,
amount=1,
)
InvoiceLinePayment.objects.create(
payment=payment2,
line=invoice_line2,
amount=1,
)
for status in ['open', 'tobepaid', 'cancelled', 'expired', 'completed']:
basket.status = status
@ -148,8 +124,6 @@ def test_basket_expiration():
# open basket, expire it immediatly after expiry_at is passed
assert CreditAssignment.objects.count() == 2
assert Payment.objects.count() == 2
assert InvoiceLinePayment.objects.count() == 2
basket.status = 'open'
basket.expiry_at = now()
basket.save()
@ -162,8 +136,6 @@ def test_basket_expiration():
invoice.refresh_from_db()
assert invoice.cancelled_at is not None
assert CreditAssignment.objects.count() == 1
assert Payment.objects.count() == 1
assert InvoiceLinePayment.objects.count() == 1
# tobepaid basket, expire it 1 hour after expiry_at is passed
line.expiration_callback_url = 'http://expiration1.com'
@ -211,7 +183,6 @@ def test_basket_expiration():
def test_basket_amounts_with_draft_invoice():
regie = Regie.objects.create(label='Foo')
PaymentType.create_defaults(regie)
draft_invoice = DraftInvoice.objects.create(
regie=regie,
date_publication=datetime.date(2023, 4, 21),
@ -230,12 +201,6 @@ def test_basket_amounts_with_draft_invoice():
assert basket.credit_amount == 0
assert basket.remaining_amount == 0
def reset_cached_properties():
basket.draft_invoice.refresh_from_db()
del basket.total_amount
del basket.credit_amount
del basket.remaining_amount
# basket with items, no credits
DraftInvoiceLine.objects.create(
slug='event-a',
@ -245,7 +210,7 @@ def test_basket_amounts_with_draft_invoice():
quantity=1,
unit_amount=1,
)
reset_cached_properties()
basket.draft_invoice.refresh_from_db()
assert basket.total_amount == 1
assert basket.credit_amount == 0
assert basket.remaining_amount == 1
@ -258,7 +223,7 @@ def test_basket_amounts_with_draft_invoice():
quantity=9,
unit_amount=1,
)
reset_cached_properties()
basket.draft_invoice.refresh_from_db()
assert basket.total_amount == 10
assert basket.credit_amount == 0
assert basket.remaining_amount == 10
@ -268,7 +233,7 @@ def test_basket_amounts_with_draft_invoice():
regie=regie,
payer_external_id='payer:1',
)
reset_cached_properties()
basket.draft_invoice.refresh_from_db()
assert basket.total_amount == 10
assert basket.credit_amount == 0
assert basket.remaining_amount == 10
@ -280,7 +245,7 @@ def test_basket_amounts_with_draft_invoice():
quantity=1,
unit_amount=1,
)
reset_cached_properties()
basket.draft_invoice.refresh_from_db()
assert basket.total_amount == 10
assert basket.credit_amount == -1
assert basket.remaining_amount == 9
@ -296,7 +261,7 @@ def test_basket_amounts_with_draft_invoice():
quantity=1,
unit_amount=1,
)
reset_cached_properties()
basket.draft_invoice.refresh_from_db()
assert basket.total_amount == 10
assert basket.credit_amount == -1
assert basket.remaining_amount == 9
@ -313,7 +278,7 @@ def test_basket_amounts_with_draft_invoice():
quantity=1,
unit_amount=1,
)
reset_cached_properties()
basket.draft_invoice.refresh_from_db()
assert basket.total_amount == 10
assert basket.credit_amount == -1
assert basket.remaining_amount == 9
@ -325,7 +290,7 @@ def test_basket_amounts_with_draft_invoice():
quantity=9,
unit_amount=1,
)
reset_cached_properties()
basket.draft_invoice.refresh_from_db()
assert basket.total_amount == 10
assert basket.credit_amount == -10
assert basket.remaining_amount == 0
@ -337,23 +302,32 @@ def test_basket_amounts_with_draft_invoice():
quantity=1,
unit_amount=1,
)
reset_cached_properties()
basket.draft_invoice.refresh_from_db()
assert basket.total_amount == 10
assert basket.credit_amount == -10
assert basket.remaining_amount == 0
# credit with assignment
payment = Payment.objects.create(
invoice = Invoice.objects.create(
regie=regie,
amount=10,
payment_type=PaymentType.objects.get(regie=regie, slug='credit'),
date_publication=datetime.date(2023, 4, 21),
date_payment_deadline=datetime.date(2023, 4, 22),
date_due=datetime.date(2023, 4, 23),
)
InvoiceLine.objects.create(
slug='event-a',
label='Event A',
event_date=datetime.date(2022, 9, 1),
invoice=invoice,
quantity=10,
unit_amount=1,
)
CreditAssignment.objects.create(
payment=payment,
invoice=invoice,
credit=credit,
amount=2,
)
reset_cached_properties()
basket.draft_invoice.refresh_from_db()
assert basket.total_amount == 10
assert basket.credit_amount == -9
assert basket.remaining_amount == 1
@ -361,7 +335,6 @@ def test_basket_amounts_with_draft_invoice():
def test_basket_amounts_with_invoice():
regie = Regie.objects.create(label='Foo')
PaymentType.create_defaults(regie)
draft_invoice = DraftInvoice.objects.create(
regie=regie,
date_publication=datetime.date(2023, 4, 21),
@ -387,14 +360,8 @@ def test_basket_amounts_with_invoice():
assert basket.credit_amount == 0
assert basket.remaining_amount == 0
def reset_cached_properties():
basket.invoice.refresh_from_db()
del basket.total_amount
del basket.credit_amount
del basket.remaining_amount
# basket with items, no credits
line1 = InvoiceLine.objects.create(
InvoiceLine.objects.create(
slug='event-a',
label='Event A',
event_date=datetime.date(2022, 9, 1),
@ -402,12 +369,12 @@ def test_basket_amounts_with_invoice():
quantity=1,
unit_amount=1,
)
reset_cached_properties()
basket.invoice.refresh_from_db()
assert basket.total_amount == 1
assert basket.credit_amount == 0
assert basket.remaining_amount == 1
line2 = InvoiceLine.objects.create(
InvoiceLine.objects.create(
slug='event-a',
label='Event A',
event_date=datetime.date(2022, 9, 1),
@ -415,7 +382,7 @@ def test_basket_amounts_with_invoice():
quantity=9,
unit_amount=1,
)
reset_cached_properties()
basket.invoice.refresh_from_db()
assert basket.total_amount == 10
assert basket.credit_amount == 0
assert basket.remaining_amount == 10
@ -425,7 +392,7 @@ def test_basket_amounts_with_invoice():
regie=regie,
payer_external_id='payer:1',
)
reset_cached_properties()
basket.invoice.refresh_from_db()
assert basket.total_amount == 10
assert basket.credit_amount == 0
assert basket.remaining_amount == 10
@ -437,22 +404,17 @@ def test_basket_amounts_with_invoice():
quantity=1,
unit_amount=1,
)
reset_cached_properties()
basket.invoice.refresh_from_db()
assert basket.total_amount == 10
assert basket.credit_amount == 0
assert basket.remaining_amount == 10
payment = Payment.objects.create(
regie=regie,
amount=10,
payment_type=PaymentType.objects.get(regie=regie, slug='credit'),
)
InvoiceLinePayment.objects.create(
payment=payment,
line=line1,
CreditAssignment.objects.create(
invoice=invoice,
credit=credit,
amount=1,
)
reset_cached_properties()
basket.invoice.refresh_from_db()
assert basket.total_amount == 10
assert basket.credit_amount == -1
assert basket.remaining_amount == 9
@ -464,12 +426,12 @@ def test_basket_amounts_with_invoice():
quantity=9,
unit_amount=1,
)
InvoiceLinePayment.objects.create(
payment=payment,
line=line2,
CreditAssignment.objects.create(
invoice=invoice,
credit=credit,
amount=9,
)
reset_cached_properties()
basket.invoice.refresh_from_db()
assert basket.total_amount == 10
assert basket.credit_amount == -10
assert basket.remaining_amount == 0

View File

@ -8,7 +8,16 @@ from django.utils.timezone import now
from lingo.basket.models import Basket, BasketLine, BasketLineItem
from lingo.epayment.models import PaymentBackend, Transaction
from lingo.invoicing.models import DraftInvoice, DraftInvoiceLine, Invoice, Payment, PaymentType, Regie
from lingo.invoicing.models import (
Credit,
CreditAssignment,
CreditLine,
DraftInvoice,
DraftInvoiceLine,
Payment,
PaymentType,
Regie,
)
from tests.utils import login
pytestmark = pytest.mark.django_db
@ -230,3 +239,128 @@ def test_basket_payment(app, simple_user):
assert payment.get_invoice_payments()[0].invoice.paid_amount == 10
assert payment.get_invoice_payments()[0].invoice.remaining_amount == 0
assert {x[0][0].url for x in mock_send.call_args_list} == {'http://payment1.com/'}
def test_basket_payment_with_assigned_credits(app, simple_user):
app = login(app, username='user', password='user')
regie = Regie.objects.create(label='Foo')
PaymentType.create_defaults(regie)
PaymentBackend.objects.create(label='Test', service='dummy', regie=regie)
invoice = DraftInvoice.objects.create(
regie=regie,
date_publication=datetime.date(2023, 4, 21),
date_payment_deadline=datetime.date(2023, 4, 22),
date_due=datetime.date(2023, 4, 23),
)
DraftInvoiceLine.objects.create(
slug='event-a-foo-bar',
label='Event A',
event_date=datetime.date(2022, 9, 1),
invoice=invoice,
quantity=10,
unit_amount=1,
user_external_id='user:1',
)
basket = Basket.objects.create(
regie=regie,
draft_invoice=invoice,
payer_nameid='ab' * 16,
payer_external_id='payer:1',
expiry_at=now() + datetime.timedelta(hours=1),
)
line = BasketLine.objects.create(
basket=basket,
closed=False,
user_external_id='user:1',
payment_callback_url='http://payment1.com',
)
BasketLineItem.objects.create(
line=line,
label='Event A',
subject='Réservation',
details='Lun 06/11, Mar 07/11',
quantity=10,
unit_amount=1,
)
line.closed = True
line.save()
credit1 = Credit.objects.create(
regie=regie,
payer_external_id='payer:1',
)
CreditLine.objects.create(
credit=credit1,
event_date=datetime.date(2022, 9, 1),
quantity=1,
unit_amount=1,
)
credit2 = Credit.objects.create(
regie=regie,
payer_external_id='payer:1',
)
CreditLine.objects.create(
credit=credit2,
event_date=datetime.date(2022, 9, 1),
quantity=3,
unit_amount=1,
)
other_regie = Regie.objects.create(label='Foo')
other_credit = Credit.objects.create(
regie=other_regie, # other regie
payer_external_id='payer:1',
)
CreditLine.objects.create(
credit=other_credit,
event_date=datetime.date(2022, 9, 1),
quantity=3,
unit_amount=1,
)
other_credit = Credit.objects.create(
regie=regie,
payer_external_id='payer:2', # other payer
)
CreditLine.objects.create(
credit=other_credit,
event_date=datetime.date(2022, 9, 1),
quantity=3,
unit_amount=1,
)
resp = app.get('/basket/validate/')
resp = resp.form.submit()
assert resp.location.startswith('https://dummy-payment.entrouvert.com/')
assert Transaction.objects.all().count() == 1
transaction = Transaction.objects.all().first()
assert transaction.status == 0
assert transaction.amount == 6
with mock.patch('lingo.utils.requests_wrapper.RequestsSession.send') as mock_send:
app.get(
reverse('lingo-epayment-explicit-return', kwargs={'transaction_id': transaction.id})
+ f'?transaction_id={transaction.order_id}&origin=origin&ok=1&signed=1'
).follow()
assert Payment.objects.count() == 2
payment1, payment2 = Payment.objects.all().order_by('pk')
assert payment1.amount == 6
assert payment1.payment_type.slug == 'creditcard'
assert payment1.get_invoice_payments()[0].invoice.paid_amount == 10
assert payment1.get_invoice_payments()[0].invoice.remaining_amount == 0
assert payment2.amount == 4
assert payment2.payment_type.slug == 'credit'
assert payment2.get_invoice_payments()[0].invoice.paid_amount == 10
assert payment2.get_invoice_payments()[0].invoice.remaining_amount == 0
assert {x[0][0].url for x in mock_send.call_args_list} == {'http://payment1.com/'}
assert payment1.get_invoice_payments()[0].invoice == payment2.get_invoice_payments()[0].invoice
assert CreditAssignment.objects.count() == 2
assignment1, assignment2 = CreditAssignment.objects.all().order_by('pk')
assert assignment1.amount == 1
assert assignment1.invoice == payment2.get_invoice_payments()[0].invoice
assert assignment1.payment == payment2
assert assignment1.credit == credit1
assert assignment2.amount == 3
assert assignment2.invoice == payment2.get_invoice_payments()[0].invoice
assert assignment2.payment == payment2
assert assignment2.credit == credit2

View File

@ -2147,6 +2147,12 @@ def test_regie_credits(app, admin_user):
Agenda.objects.create(label='Agenda A', regie=regie)
Agenda.objects.create(label='Agenda B', regie=regie)
PaymentType.create_defaults(regie)
invoice = Invoice.objects.create(
date_publication=datetime.date(2022, 10, 1),
date_payment_deadline=datetime.date(2022, 10, 31),
date_due=datetime.date(2022, 10, 31),
regie=regie,
)
credit1 = Credit.objects.create(
regie=regie,
payer_external_id='payer:1',
@ -2217,6 +2223,7 @@ def test_regie_credits(app, admin_user):
payment1.set_number()
payment1.save()
credit_assignment1 = CreditAssignment.objects.create(
invoice=invoice,
payment=payment1,
credit=credit1,
amount=1,
@ -2244,6 +2251,7 @@ def test_regie_credits(app, admin_user):
payment2.set_number()
payment2.save()
credit_assignment2 = CreditAssignment.objects.create(
invoice=invoice,
payment=payment2,
credit=credit2,
amount=0.5,
@ -2256,6 +2264,7 @@ def test_regie_credits(app, admin_user):
payment3.set_number()
payment3.save()
credit_assignment3 = CreditAssignment.objects.create(
invoice=invoice,
payment=payment3,
credit=credit2,
amount=0.5,

View File

@ -693,12 +693,19 @@ def test_credit_assignments():
assert credit2.assigned_amount == 0
assert credit2.remaining_amount == 25
invoice = Invoice.objects.create(
date_publication=datetime.date(2022, 10, 1),
date_payment_deadline=datetime.date(2022, 10, 31),
date_due=datetime.date(2022, 10, 31),
regie=regie,
)
payment1 = Payment.objects.create(
regie=regie,
amount=17,
payment_type=PaymentType.objects.get(regie=regie, slug='credit'),
)
CreditAssignment.objects.create(
invoice=invoice,
payment=payment1,
amount=7,
credit=credit1,
@ -713,6 +720,7 @@ def test_credit_assignments():
assert credit2.remaining_amount == 25
CreditAssignment.objects.create(
invoice=invoice,
payment=payment1,
amount=10,
credit=credit2,
@ -732,6 +740,7 @@ def test_credit_assignments():
payment_type=PaymentType.objects.get(regie=regie, slug='check'),
)
CreditAssignment.objects.create(
invoice=invoice,
payment=payment2,
amount=15,
credit=credit1,
@ -746,6 +755,7 @@ def test_credit_assignments():
assert credit2.remaining_amount == 15
credit_assignment = CreditAssignment.objects.create(
invoice=invoice,
payment=payment2,
amount=20,
credit=credit1,
@ -763,6 +773,7 @@ def test_credit_assignments():
with transaction.atomic():
with pytest.raises(IntegrityError) as excinfo:
CreditAssignment.objects.create(
invoice=invoice,
payment=payment2,
amount=25.01,
credit=credit2,
@ -812,6 +823,7 @@ def test_credit_assignments():
assert credit2.remaining_amount == 50
credit_assignment = CreditAssignment.objects.create(
invoice=invoice,
payment=payment2,
amount=-5,
credit=credit2,
@ -826,6 +838,7 @@ def test_credit_assignments():
assert credit2.remaining_amount == 55
credit_assignment = CreditAssignment.objects.create(
invoice=invoice,
payment=payment2,
amount=-5,
credit=credit2,