From ca8f67c2f3961101462e341085befc6af582d800 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Mon, 25 Apr 2016 07:13:37 +1000 Subject: [PATCH 1/7] Adds for_id, which lets you get an InvoiceController or CreditNoteController by the ID of the invoice/credit note/. Closes #38. --- registrasion/controllers/credit_note.py | 6 +++++- registrasion/controllers/for_id.py | 24 ++++++++++++++++++++++++ registrasion/controllers/invoice.py | 4 +++- registrasion/tests/test_invoice.py | 14 ++++++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 registrasion/controllers/for_id.py diff --git a/registrasion/controllers/credit_note.py b/registrasion/controllers/credit_note.py index e1f0ed2b..182c10e9 100644 --- a/registrasion/controllers/credit_note.py +++ b/registrasion/controllers/credit_note.py @@ -2,8 +2,12 @@ from django.db import transaction from registrasion.models import commerce +from for_id import ForId -class CreditNoteController(object): + +class CreditNoteController(ForId, object): + + __MODEL__ = commerce.CreditNote def __init__(self, credit_note): self.credit_note = credit_note diff --git a/registrasion/controllers/for_id.py b/registrasion/controllers/for_id.py new file mode 100644 index 00000000..3748b151 --- /dev/null +++ b/registrasion/controllers/for_id.py @@ -0,0 +1,24 @@ +from django.core.exceptions import ObjectDoesNotExist +from django.http import Http404 + + +class ForId(object): + ''' Mixin class that gives you new classmethods: for_id for_id_or_404. + These let you retrieve an instance of the class by specifying the model ID. + + Your subclass must define __MODEL__ as a class attribute. This will be the + model class that we wrap. There must also be a constructor that takes a + single argument: the instance of the model that we are controlling. ''' + + @classmethod + def for_id(cls, id_): + id_ = int(id_) + obj = cls.__MODEL__.objects.get(pk=id_) + return cls(obj) + + @classmethod + def for_id_or_404(cls, id_): + try: + return cls.for_id(id_) + except ObjectDoesNotExist: + return Http404 diff --git a/registrasion/controllers/invoice.py b/registrasion/controllers/invoice.py index fff97e8d..d2b6bf3f 100644 --- a/registrasion/controllers/invoice.py +++ b/registrasion/controllers/invoice.py @@ -11,9 +11,11 @@ from registrasion.models import people from cart import CartController from credit_note import CreditNoteController +from for_id import ForId +class InvoiceController(ForId, object): -class InvoiceController(object): + __MODEL__ = commerce.Invoice def __init__(self, invoice): self.invoice = invoice diff --git a/registrasion/tests/test_invoice.py b/registrasion/tests/test_invoice.py index 3a655bb1..8e1c7ac0 100644 --- a/registrasion/tests/test_invoice.py +++ b/registrasion/tests/test_invoice.py @@ -53,6 +53,20 @@ class InvoiceTestCase(RegistrationCartTestCase): self.PROD_1.price + self.PROD_2.price, invoice_2.invoice.value) + def test_invoice_controller_for_id_works(self): + current_cart = TestingCartController.for_user(self.USER_1) + current_cart.add_to_cart(self.PROD_1, 1) + + invoice = TestingInvoiceController.for_cart(current_cart.cart) + + id_ = invoice.invoice.id + + invoice1 = TestingInvoiceController.for_id(id_) + invoice2 = TestingInvoiceController.for_id(str(id_)) + + self.assertEqual(invoice.invoice, invoice1.invoice) + self.assertEqual(invoice.invoice, invoice2.invoice) + def test_create_invoice_fails_if_cart_invalid(self): self.make_ceiling("Limit ceiling", limit=1) self.set_time(datetime.datetime(2015, 01, 01, tzinfo=UTC)) From 9f72b67510ec06f2a3788bb6c56f249c8694e9ab Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Mon, 25 Apr 2016 07:14:14 +1000 Subject: [PATCH 2/7] Uses for_id_or_404 in views.py --- registrasion/views.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/registrasion/views.py b/registrasion/views.py index ea4acc8b..6a67a44b 100644 --- a/registrasion/views.py +++ b/registrasion/views.py @@ -481,10 +481,7 @@ def invoice(request, invoice_id, access_code=None): access code. ''' - invoice_id = int(invoice_id) - inv = commerce.Invoice.objects.get(pk=invoice_id) - - current_invoice = InvoiceController(inv) + current_invoice = InvoiceController.for_id_or_404(invoice_id) if not current_invoice.can_view( user=request.user, @@ -508,9 +505,7 @@ def manual_payment(request, invoice_id): if not request.user.is_staff: raise Http404() - invoice_id = int(invoice_id) - inv = get_object_or_404(commerce.Invoice, pk=invoice_id) - current_invoice = InvoiceController(inv) + current_invoice = InvoiceController.for_id_or_404(invoice_id) form = forms.ManualPaymentForm( request.POST or None, @@ -539,9 +534,7 @@ def refund(request, invoice_id): if not request.user.is_staff: raise Http404() - invoice_id = int(invoice_id) - inv = get_object_or_404(commerce.Invoice, pk=invoice_id) - current_invoice = InvoiceController(inv) + current_invoice = InvoiceController.for_id_or_404(invoice_id) try: current_invoice.refund() @@ -560,10 +553,7 @@ def credit_note(request, note_id, access_code=None): if not request.user.is_staff: raise Http404() - note_id = int(note_id) - note = commerce.CreditNote.objects.get(pk=note_id) - - current_note = CreditNoteController(note) + current_note = CreditNoteController.for_id_or_404(note_id) apply_form = forms.ApplyCreditNoteForm( note.invoice.user, From 67b047e7b30e0b10306b4723771a85cf5592a436 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Mon, 25 Apr 2016 07:15:03 +1000 Subject: [PATCH 3/7] Simplifies invoice-getting documentation. --- docs/payments.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/payments.rst b/docs/payments.rst index 51ff01da..3a2d6262 100644 --- a/docs/payments.rst +++ b/docs/payments.rst @@ -32,10 +32,7 @@ Our the ``demopay`` view from the ``registrasion-demo`` project implements pre-v from registrasion.controllers.invoice import InvoiceController from django.core.exceptions import ValidationError - # Get the Registrasion Invoice model - inv = get_object_or_404(rego.Invoice.objects, pk=invoice_id) - - invoice = InvoiceController(inv) + invoice = InvoiceController.for_id_or_404(invoice.id) try: invoice.validate_allowed_to_pay() # Verify that we're allowed to do this. From 9a4574ef2c3b5e41cc8e45ee171be734bb0a11f2 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Mon, 25 Apr 2016 07:42:05 +1000 Subject: [PATCH 4/7] DRYs up test_invoice a bit --- registrasion/tests/test_invoice.py | 98 ++++++++---------------------- 1 file changed, 25 insertions(+), 73 deletions(-) diff --git a/registrasion/tests/test_invoice.py b/registrasion/tests/test_invoice.py index 8e1c7ac0..b64ae58c 100644 --- a/registrasion/tests/test_invoice.py +++ b/registrasion/tests/test_invoice.py @@ -18,6 +18,12 @@ UTC = pytz.timezone('UTC') class InvoiceTestCase(RegistrationCartTestCase): + def _invoice_containing_prod_1(self, qty=1): + cart = TestingCartController.for_user(self.USER_1) + cart.add_to_cart(self.PROD_1, qty) + + return TestingInvoiceController.for_cart(self.reget(cart.cart)) + def test_create_invoice(self): current_cart = TestingCartController.for_user(self.USER_1) @@ -54,10 +60,7 @@ class InvoiceTestCase(RegistrationCartTestCase): invoice_2.invoice.value) def test_invoice_controller_for_id_works(self): - current_cart = TestingCartController.for_user(self.USER_1) - current_cart.add_to_cart(self.PROD_1, 1) - - invoice = TestingInvoiceController.for_cart(current_cart.cart) + invoice = self._invoice_containing_prod_1(1) id_ = invoice.invoice.id @@ -82,10 +85,8 @@ class InvoiceTestCase(RegistrationCartTestCase): TestingInvoiceController.for_cart(current_cart.cart) def test_paying_invoice_makes_new_cart(self): - current_cart = TestingCartController.for_user(self.USER_1) - current_cart.add_to_cart(self.PROD_1, 1) + invoice = self._invoice_containing_prod_1(1) - invoice = TestingInvoiceController.for_cart(current_cart.cart) invoice.pay("A payment!", invoice.invoice.value) # This payment is for the correct amount invoice should be paid. @@ -96,7 +97,7 @@ class InvoiceTestCase(RegistrationCartTestCase): # Asking for a cart should generate a new one new_cart = TestingCartController.for_user(self.USER_1) - self.assertNotEqual(current_cart.cart, new_cart.cart) + self.assertNotEqual(invoice.invoice.cart, new_cart.cart) def test_invoice_includes_discounts(self): voucher = inventory.Voucher.objects.create( @@ -181,24 +182,16 @@ class InvoiceTestCase(RegistrationCartTestCase): self.assertFalse(invoice_2_new.invoice.is_void) def test_voiding_invoice_creates_new_invoice(self): - current_cart = TestingCartController.for_user(self.USER_1) - - # Should be able to create an invoice after the product is added - current_cart.add_to_cart(self.PROD_1, 1) - invoice_1 = TestingInvoiceController.for_cart(current_cart.cart) + invoice_1 = self._invoice_containing_prod_1(1) self.assertFalse(invoice_1.invoice.is_void) invoice_1.void() - invoice_2 = TestingInvoiceController.for_cart(current_cart.cart) + invoice_2 = TestingInvoiceController.for_cart(invoice_1.invoice.cart) self.assertNotEqual(invoice_1.invoice, invoice_2.invoice) def test_cannot_pay_void_invoice(self): - current_cart = TestingCartController.for_user(self.USER_1) - - # Should be able to create an invoice after the product is added - current_cart.add_to_cart(self.PROD_1, 1) - invoice_1 = TestingInvoiceController.for_cart(current_cart.cart) + invoice_1 = self._invoice_containing_prod_1(1) invoice_1.void() @@ -206,11 +199,7 @@ class InvoiceTestCase(RegistrationCartTestCase): invoice_1.validate_allowed_to_pay() def test_cannot_void_paid_invoice(self): - current_cart = TestingCartController.for_user(self.USER_1) - - # Should be able to create an invoice after the product is added - current_cart.add_to_cart(self.PROD_1, 1) - invoice = TestingInvoiceController.for_cart(current_cart.cart) + invoice = self._invoice_containing_prod_1(1) invoice.pay("Reference", invoice.invoice.value) @@ -218,11 +207,7 @@ class InvoiceTestCase(RegistrationCartTestCase): invoice.void() def test_cannot_void_partially_paid_invoice(self): - current_cart = TestingCartController.for_user(self.USER_1) - - # Should be able to create an invoice after the product is added - current_cart.add_to_cart(self.PROD_1, 1) - invoice = TestingInvoiceController.for_cart(current_cart.cart) + invoice = self._invoice_containing_prod_1(1) invoice.pay("Reference", invoice.invoice.value - 1) self.assertTrue(invoice.invoice.is_unpaid) @@ -247,10 +232,7 @@ class InvoiceTestCase(RegistrationCartTestCase): invoice.validate_allowed_to_pay() def test_overpaid_invoice_results_in_credit_note(self): - cart = TestingCartController.for_user(self.USER_1) - cart.add_to_cart(self.PROD_1, 1) - - invoice = TestingInvoiceController.for_cart(self.reget(cart.cart)) + invoice = self._invoice_containing_prod_1(1) # Invoice is overpaid by 1 unit to_pay = invoice.invoice.value + 1 @@ -268,10 +250,7 @@ class InvoiceTestCase(RegistrationCartTestCase): self.assertEqual(to_pay - invoice.invoice.value, credit_notes[0].value) def test_full_paid_invoice_does_not_generate_credit_note(self): - cart = TestingCartController.for_user(self.USER_1) - cart.add_to_cart(self.PROD_1, 1) - - invoice = TestingInvoiceController.for_cart(self.reget(cart.cart)) + invoice = self._invoice_containing_prod_1(1) # Invoice is paid evenly invoice.pay("Reference", invoice.invoice.value) @@ -287,10 +266,7 @@ class InvoiceTestCase(RegistrationCartTestCase): self.assertEqual(0, credit_notes.count()) def test_refund_partially_paid_invoice_generates_correct_credit_note(self): - cart = TestingCartController.for_user(self.USER_1) - cart.add_to_cart(self.PROD_1, 1) - - invoice = TestingInvoiceController.for_cart(self.reget(cart.cart)) + invoice = self._invoice_containing_prod_1(1) # Invoice is underpaid by 1 unit to_pay = invoice.invoice.value - 1 @@ -309,10 +285,7 @@ class InvoiceTestCase(RegistrationCartTestCase): self.assertEqual(to_pay, credit_notes[0].value) def test_refund_fully_paid_invoice_generates_correct_credit_note(self): - cart = TestingCartController.for_user(self.USER_1) - cart.add_to_cart(self.PROD_1, 1) - - invoice = TestingInvoiceController.for_cart(self.reget(cart.cart)) + invoice = self._invoice_containing_prod_1(1) to_pay = invoice.invoice.value invoice.pay("Reference", to_pay) @@ -332,10 +305,7 @@ class InvoiceTestCase(RegistrationCartTestCase): self.assertEqual(to_pay, credit_notes[0].value) def test_apply_credit_note_pays_invoice(self): - cart = TestingCartController.for_user(self.USER_1) - cart.add_to_cart(self.PROD_1, 1) - - invoice = TestingInvoiceController.for_cart(self.reget(cart.cart)) + invoice = self._invoice_containing_prod_1(1) to_pay = invoice.invoice.value invoice.pay("Reference", to_pay) @@ -363,10 +333,7 @@ class InvoiceTestCase(RegistrationCartTestCase): self.assertEquals(0, commerce.CreditNote.unclaimed().count()) def test_apply_credit_note_generates_new_credit_note_if_overpaying(self): - cart = TestingCartController.for_user(self.USER_1) - cart.add_to_cart(self.PROD_1, 2) - - invoice = TestingInvoiceController.for_cart(self.reget(cart.cart)) + invoice = self._invoice_containing_prod_1(2) to_pay = invoice.invoice.value invoice.pay("Reference", to_pay) @@ -405,10 +372,7 @@ class InvoiceTestCase(RegistrationCartTestCase): ) def test_cannot_apply_credit_note_on_invalid_invoices(self): - cart = TestingCartController.for_user(self.USER_1) - cart.add_to_cart(self.PROD_1, 1) - - invoice = TestingInvoiceController.for_cart(self.reget(cart.cart)) + invoice = self._invoice_containing_prod_1(1) to_pay = invoice.invoice.value invoice.pay("Reference", to_pay) @@ -421,10 +385,7 @@ class InvoiceTestCase(RegistrationCartTestCase): cn = TestingCreditNoteController(credit_note) # Create a new cart with invoice, pay it - cart = TestingCartController.for_user(self.USER_1) - cart.add_to_cart(self.PROD_1, 1) - - invoice_2 = TestingInvoiceController.for_cart(self.reget(cart.cart)) + invoice_2 = self._invoice_containing_prod_1(1) invoice_2.pay("LOL", invoice_2.invoice.value) # Cannot pay paid invoice @@ -437,20 +398,14 @@ class InvoiceTestCase(RegistrationCartTestCase): cn.apply_to_invoice(invoice_2.invoice) # Create a new cart with invoice - cart = TestingCartController.for_user(self.USER_1) - cart.add_to_cart(self.PROD_1, 1) - - invoice_2 = TestingInvoiceController.for_cart(self.reget(cart.cart)) + invoice_2 = self._invoice_containing_prod_1(1) invoice_2.void() # Cannot pay void invoice with self.assertRaises(ValidationError): cn.apply_to_invoice(invoice_2.invoice) def test_cannot_apply_a_refunded_credit_note(self): - cart = TestingCartController.for_user(self.USER_1) - cart.add_to_cart(self.PROD_1, 1) - - invoice = TestingInvoiceController.for_cart(self.reget(cart.cart)) + invoice = self._invoice_containing_prod_1(1) to_pay = invoice.invoice.value invoice.pay("Reference", to_pay) @@ -479,10 +434,7 @@ class InvoiceTestCase(RegistrationCartTestCase): cn.apply_to_invoice(invoice_2.invoice) def test_cannot_refund_an_applied_credit_note(self): - cart = TestingCartController.for_user(self.USER_1) - cart.add_to_cart(self.PROD_1, 1) - - invoice = TestingInvoiceController.for_cart(self.reget(cart.cart)) + invoice = self._invoice_containing_prod_1(1) to_pay = invoice.invoice.value invoice.pay("Reference", to_pay) From 12e04c248fb1969a0796a03e493e1a14569d18ec Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Mon, 25 Apr 2016 08:13:44 +1000 Subject: [PATCH 5/7] Credit notes are now generated when invoices are overpaid, or invoices are paid into void or refunded invoices. Closes #37. --- registrasion/controllers/invoice.py | 17 +++++-- registrasion/tests/controller_helpers.py | 7 ++- registrasion/tests/test_invoice.py | 58 +++++++++++++++++++----- 3 files changed, 64 insertions(+), 18 deletions(-) diff --git a/registrasion/controllers/invoice.py b/registrasion/controllers/invoice.py index d2b6bf3f..ef4ee320 100644 --- a/registrasion/controllers/invoice.py +++ b/registrasion/controllers/invoice.py @@ -13,6 +13,7 @@ from cart import CartController from credit_note import CreditNoteController from for_id import ForId + class InvoiceController(ForId, object): __MODEL__ = commerce.Invoice @@ -195,11 +196,6 @@ class InvoiceController(ForId, object): # Invoice no longer has amount owing self._mark_paid() - if remainder < 0: - CreditNoteController.generate_from_invoice( - self.invoice, - 0 - remainder, - ) elif total_paid == 0 and num_payments > 0: # Invoice has multiple payments totalling zero self._mark_void() @@ -215,6 +211,17 @@ class InvoiceController(ForId, object): # Should not ever change from here pass + # Generate credit notes from residual payments + residual = 0 + if self.invoice.is_paid: + if remainder < 0: + residual = 0 - remainder + elif self.invoice.is_void or self.invoice.is_refunded: + residual = total_paid + + if residual != 0: + CreditNoteController.generate_from_invoice(self.invoice, residual) + def _mark_paid(self): ''' Marks the invoice as paid, and updates the attached cart if necessary. ''' diff --git a/registrasion/tests/controller_helpers.py b/registrasion/tests/controller_helpers.py index ac676c03..3ede49c2 100644 --- a/registrasion/tests/controller_helpers.py +++ b/registrasion/tests/controller_helpers.py @@ -34,11 +34,14 @@ class TestingCartController(CartController): class TestingInvoiceController(InvoiceController): - def pay(self, reference, amount): + def pay(self, reference, amount, pre_validate=True): ''' Testing method for simulating an invoice paymenht by the given amount. ''' - self.validate_allowed_to_pay() + if pre_validate: + # Manual payments don't pre-validate; we should test that things + # still work if we do silly things. + self.validate_allowed_to_pay() ''' Adds a payment ''' commerce.ManualPayment.objects.create( diff --git a/registrasion/tests/test_invoice.py b/registrasion/tests/test_invoice.py index b64ae58c..a7db2849 100644 --- a/registrasion/tests/test_invoice.py +++ b/registrasion/tests/test_invoice.py @@ -24,6 +24,10 @@ class InvoiceTestCase(RegistrationCartTestCase): return TestingInvoiceController.for_cart(self.reget(cart.cart)) + def _credit_note_for_invoice(self, invoice): + note = commerce.CreditNote.objects.get(invoice=invoice) + return TestingCreditNoteController(note) + def test_create_invoice(self): current_cart = TestingCartController.for_user(self.USER_1) @@ -314,8 +318,7 @@ class InvoiceTestCase(RegistrationCartTestCase): invoice.refund() # There should be one credit note generated out of the invoice. - credit_note = commerce.CreditNote.objects.get(invoice=invoice.invoice) - cn = TestingCreditNoteController(credit_note) + cn = self._credit_note_for_invoice(invoice.invoice) # That credit note should be in the unclaimed pile self.assertEquals(1, commerce.CreditNote.unclaimed().count()) @@ -342,8 +345,7 @@ class InvoiceTestCase(RegistrationCartTestCase): invoice.refund() # There should be one credit note generated out of the invoice. - credit_note = commerce.CreditNote.objects.get(invoice=invoice.invoice) - cn = TestingCreditNoteController(credit_note) + cn = self._credit_note_for_invoice(invoice.invoice) self.assertEquals(1, commerce.CreditNote.unclaimed().count()) @@ -381,8 +383,7 @@ class InvoiceTestCase(RegistrationCartTestCase): invoice.refund() # There should be one credit note generated out of the invoice. - credit_note = commerce.CreditNote.objects.get(invoice=invoice.invoice) - cn = TestingCreditNoteController(credit_note) + cn = self._credit_note_for_invoice(invoice.invoice) # Create a new cart with invoice, pay it invoice_2 = self._invoice_containing_prod_1(1) @@ -415,9 +416,8 @@ class InvoiceTestCase(RegistrationCartTestCase): self.assertEquals(1, commerce.CreditNote.unclaimed().count()) - credit_note = commerce.CreditNote.objects.get(invoice=invoice.invoice) + cn = self._credit_note_for_invoice(invoice.invoice) - cn = TestingCreditNoteController(credit_note) cn.refund() # Refunding a credit note should mark it as claimed @@ -444,9 +444,7 @@ class InvoiceTestCase(RegistrationCartTestCase): self.assertEquals(1, commerce.CreditNote.unclaimed().count()) - credit_note = commerce.CreditNote.objects.get(invoice=invoice.invoice) - - cn = TestingCreditNoteController(credit_note) + cn = self._credit_note_for_invoice(invoice.invoice) # Create a new cart with invoice cart = TestingCartController.for_user(self.USER_1) @@ -460,3 +458,41 @@ class InvoiceTestCase(RegistrationCartTestCase): # Cannot refund this credit note as it is already applied. with self.assertRaises(ValidationError): cn.refund() + + def test_money_into_void_invoice_generates_credit_note(self): + invoice = self._invoice_containing_prod_1(1) + invoice.void() + + val = invoice.invoice.value + + invoice.pay("Paying into the void.", val, pre_validate=False) + cn = self._credit_note_for_invoice(invoice.invoice) + self.assertEqual(val, cn.credit_note.value) + + def test_money_into_refunded_invoice_generates_credit_note(self): + invoice = self._invoice_containing_prod_1(1) + + val = invoice.invoice.value + + invoice.pay("Paying the first time.", val) + invoice.refund() + + cnval = val - 1 + invoice.pay("Paying into the void.", cnval, pre_validate=False) + + notes = commerce.CreditNote.objects.filter(invoice=invoice.invoice) + notes = sorted(notes, key = lambda note: note.value) + + self.assertEqual(cnval, notes[0].value) + self.assertEqual(val, notes[1].value) + + def test_money_into_paid_invoice_generates_credit_note(self): + invoice = self._invoice_containing_prod_1(1) + + val = invoice.invoice.value + + invoice.pay("Paying the first time.", val) + + invoice.pay("Paying into the void.", val, pre_validate=False) + cn = self._credit_note_for_invoice(invoice.invoice) + self.assertEqual(val, cn.credit_note.value) From 00b79a4beceadbe2c6df04d7ba32b570fccadc59 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Mon, 25 Apr 2016 08:27:20 +1000 Subject: [PATCH 6/7] Documentation now reflects that issue #37 is solved. --- docs/payments.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/payments.rst b/docs/payments.rst index 3a2d6262..058b64bc 100644 --- a/docs/payments.rst +++ b/docs/payments.rst @@ -81,7 +81,7 @@ Calling ``update_status`` collects the ``PaymentBase`` objects that are attached When your invoice becomes ``PAID`` for the first time, if there's a cart of inventory items attached to it, that cart becomes permanently reserved -- that is, all of the items within it are no longer available for other users to purchase. If an invoice becomes ``REFUNDED``, the items in the cart are released, which means that they are available for anyone to purchase again. -(One GitHub Issue #37 is completed) If you overpay an invoice, or pay into an invoice that should not have funds attached, a credit note for the residual payments will also be issued. +If you overpay an invoice, or pay into an invoice that should not have funds attached, a credit note for the residual payments will also be issued. In general, although this means you *can* use negative payments to take an invoice into a *REFUNDED* state, it's still much more sensible to use the credit notes facility, as this makes sure that any leftover funds remain tracked in the system. From f309d92a24fc6f8ba6441ee5865d6ca3e0eae337 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Mon, 25 Apr 2016 08:34:54 +1000 Subject: [PATCH 7/7] Discusses access control for payments --- docs/payments.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/payments.rst b/docs/payments.rst index 058b64bc..f671895b 100644 --- a/docs/payments.rst +++ b/docs/payments.rst @@ -12,6 +12,26 @@ Registrasion also keeps track of money that is not currently attached to invoice Finally, Registrasion provides a `manual payments`_ feature, which allows for staff members to manually report payments into the system. This is the only payment facility built into Registrasion, but it's not intended as a reference implementation. +Invoice and payment access control +---------------------------------- + +Conferences are interesting: usually you want attendees to fill in their own registration so that they get their catering options right, so that they can personally agree to codes of conduct, and so that you can make sure that you're communicating key information directly with them. + +On the other hand, employees at companies often need for their employers to directly pay for their registration. + +Registrasion solves this problem by having attendees complete their own registration, and then providing an access URL that allows anyone who holds that URL to view their invoice and make payment. + +You can call ``InvoiceController.can_view`` to determine whether or not you're allowed to show the invoice. It returns true if the user is allowed to view the invoice:: + + InvoiceController.can_view(self, user=request.user, access_code="CODE") + +As a rule, you should call ``can_view`` before doing any operations that amend the status of an invoice. This includes taking payments or requesting refunds. + +The access code is unique for each attendee -- this means that every invoice that an attendee generates can be viewed with the same access code. This is useful if the user amends their registration between giving the URL to their employer, and their employer making payment. + + + + Making payments ---------------