def process_completed_payment(self, trans): manager = InvoiceManager() invoice = self.get_invoice_for_transaction(trans) def invoice_logger(msg): raise TrustlyException("Trustly invoice processing failed: {0}".format(msg)) method = trans.paymentmethod pm = method.get_implementation() manager.process_incoming_payment_for_invoice(invoice, trans.amount, 'Trustly id {0}'.format(trans.id), 0, # XXX: we pay zero now, but should perhaps support fees? pm.config('accounting_income'), pm.config('accounting_fee'), [], invoice_logger, method) TrustlyLog(message="Completed payment for Trustly id {0} (order {1}), {2}{3}, invoice {4}".format(trans.id, trans.orderid, settings.CURRENCY_ABBREV, trans.amount, invoice.id), paymentmethod=method).save() send_simple_mail(settings.INVOICE_SENDER_EMAIL, pm.config('notification_receiver'), "Trustly payment completed", "A Trustly payment for {0} of {1}{2} for invoice {3} was completed on the Trustly platform.\n\nInvoice: {4}\nRecipient name: {5}\nRecipient email: {6}\n".format( method.internaldescription, settings.CURRENCY_ABBREV, trans.amount, invoice.id, invoice.title, invoice.recipient_name, invoice.recipient_email), )
def handle(self, *args, **options): manager = InvoiceManager() with transaction.atomic(): for trans in TrustlyTransaction.objects.filter( pendingat__isnull=False, completedat__isnull=True): # Pending is set, completed is not set, means we are waiting for a slow transaction. try: invoice = Invoice.objects.get(pk=trans.invoiceid) except Invoice.DoesNotExist: raise CommandError( "Invoice {0} for order {1} not found!".format( trans.invoiceid, trans.orderid)) # Make sure the invoice is valid for at least another 24 hours (yay banks that only # sync their data once a day) # If the invoice is extended, email is sent to invoice admins, but not end users. r = manager.postpone_invoice_autocancel( invoice, timedelta(hours=24), "Trustly payment still in pending, awaiting credit", silent=False) if r: TrustlyLog( message= "Extended autocancel time for invoice {0} to ensure time for credit notification" .format(invoice.id), paymentmethod=trans.paymentmethod).save()
def pendinginvoices_cancel(request, urlname, invoiceid): conference = get_authenticated_conference(request, urlname) invoice = get_object_or_404(Invoice, pk=invoiceid, paidat__isnull=True) # Have to verify that this invoice is actually for this conference if not ( ConferenceRegistration.objects.filter(conference=conference, invoice=invoice).exists() or BulkPayment.objects.filter(conference=conference, invoice=invoice).exists() or Sponsor.objects.filter(conference=conference, invoice=invoice).exists() ): raise PermissionDenied("Invoice not for this conference") if request.method == 'POST': form = ConferenceInvoiceCancelForm(data=request.POST) if form.is_valid(): manager = InvoiceManager() try: manager.cancel_invoice(invoice, form.cleaned_data['reason'], request.user.username) messages.info(request, 'Invoice {} canceled.'.format(invoice.id)) return HttpResponseRedirect('../../') except Exception as e: messages.error(request, 'Failed to cancel invoice: {}'.format(e)) else: form = ConferenceInvoiceCancelForm() return render(request, 'confreg/admin_backend_form.html', { 'conference': conference, 'basetemplate': 'confreg/confadmin_base.html', 'form': form, 'whatverb': 'Cancel invoice', 'savebutton': 'Cancel invoice', 'cancelname': 'Return without canceling', 'cancelurl': '../../', 'note': 'Canceling invoice #{} ({}) will disconnect it from the associated objects and send a notification to the recipient of the invoice ({}).'.format(invoice.id, invoice.title, invoice.recipient_name), })
def prepaidorder_refund(request, urlname, orderid): conference = get_authenticated_conference(request, urlname) order = get_object_or_404(PurchasedVoucher, pk=orderid, conference=conference) if PrepaidBatch.objects.filter(pk=order.batch_id).aggregate(used=Count('prepaidvoucher__user'))['used'] > 0: # This link should not exist in the first place, but double check if someone # used the voucher in between the click. messages.error(request, 'Cannot refund order, there are used vouchers in the batch!') return HttpResponseRedirect("../../") invoice = order.invoice if not invoice: messages.error(request, 'Order does not have an invoice, there is nothing to refund!') return HttpResponseRedirect("../../") if not invoice.paidat: messages.error(request, 'Invoice for this order has not been paid, there is nothing to refund!') return HttpResponseRedirect("../../") if request.method == 'POST': form = PurchasedVoucherRefundForm(data=request.POST) if form.is_valid(): # Actually issue the refund manager = InvoiceManager() manager.refund_invoice(invoice, 'Prepaid order refunded', invoice.total_amount - invoice.total_vat, invoice.total_vat, conference.vat_registrations) send_conference_notification( conference, 'Prepaid order {} refunded'.format(order.id), 'Prepaid order {} purchased by {} {} has been refunded.\nNo vouchers were in use, and the order and batch have both been deleted.\n'.format(order.id, order.user.first_name, order.user.last_name), ) order.batch.delete() order.delete() messages.info(request, 'Order has been refunded and deleted.') return HttpResponseRedirect("../../") else: form = PurchasedVoucherRefundForm() if settings.EU_VAT: note = 'You are about to refund {}{} ({}{} + {}{} VAT) for invoice {}. Please confirm that this is what you want!'.format(settings.CURRENCY_SYMBOL, invoice.total_amount, settings.CURRENCY_SYMBOL, invoice.total_amount - invoice.total_vat, settings.CURRENCY_SYMBOL, invoice.total_vat, invoice.id) else: note = 'You are about to refund {}{} for invoice {}. Please confirm that this is what you want!'.format(settings.CURRENCY_SYMBOL, invoice.total_amount, invoice.id) return render(request, 'confreg/admin_backend_form.html', { 'conference': conference, 'basetemplate': 'confreg/confadmin_base.html', 'form': form, 'note': note, 'whatverb': 'Refund', 'what': 'repaid vouchers', 'savebutton': 'Refund', 'cancelurl': '../../', 'breadcrumbs': [('/events/admin/{}/prepaidorders/'.format(conference.urlname), 'Prepaid Voucher Orders'), ], 'helplink': 'vouchers', })
def _flag_invoices(request, trans, invoices, pm, fee_account): manager = InvoiceManager() invoicelog = [] transaction.set_autocommit(False) def invoicelogger(msg): invoicelog.append(msg) if len(invoices) == 1: fee = invoices[0].total_amount - trans.amount # Calculated fee else: # There can be no fees when using multiple invoices, so ensure that if sum([i.total_amount for i in invoices]) != trans.amount: raise Exception("Fees not supported for multi-invoice flagging") fee = 0 for invoice in invoices: (status, _invoice, _processor) = manager.process_incoming_payment_for_invoice( invoice, invoice.total_amount, "Bank transfer from {0} with id {1}, manually matched".format( trans.method.internaldescription, trans.methodidentifier), fee, pm.config('bankaccount'), fee_account and fee_account.num, [], invoicelogger, trans.method) if status != manager.RESULT_OK: messages.error(request, "Failed to run invoice processor:") for m in invoicelog: messages.warning(request, m) # Roll back any changes so far transaction.rollback() return False BankTransferFees(invoice=invoice, fee=fee).save() InvoiceLog( message= "Manually matched invoice {0} for {1} {2}, bank transaction {3} {2}, fees {4}" .format( invoice.id, invoice.total_amount, settings.CURRENCY_ABBREV, trans.amount, fee, )).save() # Remove the pending transaction trans.delete() transaction.commit() return True
def handle(self, *args, **options): invoices = Invoice.objects.filter(finalized=True, deleted=False, paidat__isnull=True, canceltime__lt=datetime.now()) manager = InvoiceManager() for invoice in invoices: self.stdout.write("Canceling invoice {0}, expired".format(invoice.id)) # The manager will automatically cancel any registrations etc, # as well as send an email to the user. manager.cancel_invoice(invoice, "Invoice was automatically canceled because payment was not received on time.")
def handle(self, *args, **options): refunds = InvoiceRefund.objects.filter(issued__isnull=True) for r in refunds: manager = InvoiceManager() # One transaction for each object, and make sure it's properly # locked by using select for update, in case we get a notification # delivered while we are still processing. with transaction.atomic(): rr = InvoiceRefund.objects.select_for_update().filter( pk=r.pk)[0] if not rr.invoice.can_autorefund: # How did we end up in the queue?! raise CommandError( "Invoice {0} listed for refund, but provider is not capable of refunds!" .format(r.invoice.id)) # Calling autorefund will update the InvoiceRefund object # after calling the APIs, so nothing more to do here. if manager.autorefund_invoice(rr): self.stdout.write( "Issued API refund of invoice {0}.".format( rr.invoice.pk)) else: self.stdout.write( "Failed to issue API refund for invoice {0}, will keep trying." .format(rr.invoice.pk)) # Send alerts for any refunds that have been issued but that have not completed within # 3 days (completely arbitrary, but normally it happens within seconds/minutes/hours). stalledrefunds = InvoiceRefund.objects.filter( issued__isnull=False, completed__isnull=True, issued__lt=datetime.now() - timedelta(days=3)) if stalledrefunds: send_simple_mail( settings.INVOICE_SENDER_EMAIL, settings.INVOICE_NOTIFICATION_RECEIVER, "Stalled invoice refunds", """One or more invoice refunds appear to be stalled. These refunds have been issued to the provider, but no confirmation has shown up. This requires manual investigation. The following invoices have stalled refunds: {0} Better go check! """.format("\n".join([r.invoice.invoicestr for r in stalledrefunds])))
def process_pending_payment(self, trans): # If we have received a 'pending' notification, postpone the invoice to ensure it's valid # for another 2 hours, in case the credit notification is slightly delayed. # A cronjob will run every hour to potentially further extend this. manager = InvoiceManager() invoice = self.get_invoice_for_transaction(trans) # Postpone the invoice so it's valid for at least another 2 hours. r = manager.postpone_invoice_autocancel(invoice, timedelta(hours=2), reason="Trustly pending arrived, awaiting credit", silent=True) if r: TrustlyLog(message="Extended autocancel time for invoice {0} to ensure time for credit notification".format(invoice.id), paymentmethod=trans.paymentmethod).save()
def multireg_refund(request, urlname, bulkid): conference = get_authenticated_conference(request, urlname) bulkpay = get_object_or_404(BulkPayment, pk=bulkid, conference=conference) if bulkpay.conferenceregistration_set.exists(): messages.error(request, "This bulk payment has registrations, cannot be canceled!") return HttpResponseRedirect("../../") invoice = bulkpay.invoice if not invoice: messages.error(request, "This bulk payment does not have an invoice!") return HttpResonseRedirect("../../") if not invoice.paidat: messages.error(request, "This bulk payment invoice has not been paid!") return HttpResonseRedirect("../../") if request.method == 'POST': form = BulkPaymentRefundForm(invoice, data=request.POST) if form.is_valid(): manager = InvoiceManager() manager.refund_invoice(invoice, 'Multi registration refunded', form.cleaned_data['amount'], form.cleaned_data['vatamount'], conference.vat_registrations) send_conference_notification( conference, 'Multi registration {} refunded'.format(bulkpay.id), 'Multi registration {} purchased by {} {} has been refunded.\nNo registrations were active in this multi registration, and the multi registration has now been deleted.\n'.format(bulkpay.id, bulkpay.user.first_name, bulkpay.user.last_name), ) bulkpay.delete() messages.info(request, 'Multi registration has been refunded and deleted.') return HttpResponseRedirect("../../") else: form = BulkPaymentRefundForm(invoice, initial={'amount': invoice.total_amount - invoice.total_vat, 'vatamount': invoice.total_vat}) return render(request, 'confreg/admin_backend_form.html', { 'conference': conference, 'basetemplate': 'confreg/confadmin_base.html', 'form': form, 'whatverb': 'Refund', 'what': 'multi registration', 'savebutton': 'Refund', 'cancelurl': '../../', 'breadcrumbs': [('/events/admin/{}/multiregs/'.format(conference.urlname), 'Multi Registrations'), ], 'helplink': 'registrations', })
def create_sponsor_invoice(user, sponsor, override_duedate=None): conference = sponsor.conference level = sponsor.level invoicerows, reverse_vat = _invoicerows_for_sponsor(sponsor) if override_duedate: duedate = override_duedate elif conference.startdate < today_conference() + timedelta(days=5): # If conference happens in the next 5 days, invoice is due immediately duedate = timezone.now() elif conference.startdate < today_conference() + timedelta(days=30): # Less than 30 days before the conference, set the due date to # 5 days before the conference duedate = timezone.make_aware( datetime.combine(conference.startdate - timedelta(days=5), timezone.now().time())) else: # More than 30 days before the conference, set the due date # to 30 days from now. duedate = timezone.now() + timedelta(days=30) manager = InvoiceManager() processor = invoicemodels.InvoiceProcessor.objects.get( processorname="confsponsor processor") i = manager.create_invoice( user, user.email, user.first_name + ' ' + user.last_name, get_sponsor_invoice_address(sponsor.name, sponsor.invoiceaddr, sponsor.vatnumber), '%s sponsorship' % conference.conferencename, timezone.now(), duedate, invoicerows, processor=processor, processorid=sponsor.pk, accounting_account=settings.ACCOUNTING_CONFSPONSOR_ACCOUNT, accounting_object=conference.accounting_object, reverse_vat=reverse_vat, extra_bcc_list=conference.sponsoraddr, paymentmethods=level.paymentmethods.all(), ) return i
def process_stripe_checkout(co): if co.completedat: # Already completed, so don't do anything with it return with transaction.atomic(): method = co.paymentmethod pm = method.get_implementation() api = StripeApi(pm) # Update the status from the API if api.update_checkout_status(co): # Went from unpaid to paid, so Do The Magic (TM) manager = InvoiceManager() invoice = Invoice.objects.get(pk=co.invoiceid) def invoice_logger(msg): raise StripeException( "Stripe invoice processing failed: {0}".format(msg)) manager.process_incoming_payment_for_invoice( invoice, co.amount, 'Stripe checkout id {0}'.format(co.id), co.fee, pm.config('accounting_income'), pm.config('accounting_fee'), [], invoice_logger, method) StripeLog( message= "Completed payment for Stripe id {0} ({1}{2}, invoice {3})". format(co.id, settings.CURRENCY_ABBREV, co.amount, invoice.id), paymentmethod=method).save() send_simple_mail( settings.INVOICE_SENDER_EMAIL, pm.config('notification_receiver'), "Stripe payment completed", "A Stripe payment for {0} of {1}{2} for invoice {3} was completed.\n\nInvoice: {4}\nRecipient name: {5}\nRecipient email: {6}\n" .format( method.internaldescription, settings.CURRENCY_ABBREV, co.amount, invoice.id, invoice.title, invoice.recipient_name, invoice.recipient_email, ))
def create_voucher_invoice(conference, invoiceaddr, user, rt, num): invoicerows = [ ['Voucher for "%s"' % rt.regtype, 1, rt.cost, rt.conference.vat_registrations] ] * num manager = InvoiceManager() processor = invoicemodels.InvoiceProcessor.objects.get(processorname="confsponsor voucher processor") i = manager.create_invoice( user, user.email, user.first_name + ' ' + user.last_name, invoiceaddr, 'Prepaid vouchers for %s' % conference.conferencename, datetime.now(), date.today(), invoicerows, processor=processor, accounting_account=settings.ACCOUNTING_CONFREG_ACCOUNT, accounting_object=conference.accounting_object, paymentmethods=conference.paymentmethods.all(), ) return i
def create_voucher_invoice(sponsor, user, rt, num): invoicerows = [ ['Voucher for "%s"' % rt.regtype, num, rt.cost] ] manager = InvoiceManager() processor = invoicemodels.InvoiceProcessor.objects.get(processorname="confsponsor voucher processor") i = manager.create_invoice( user, user.email, user.first_name + ' ' + user.last_name, sponsor.invoiceaddr, 'Prepaid vouchers for %s' % sponsor.conference.conferencename, datetime.now(), date.today(), invoicerows, processor = processor, bankinfo = False, accounting_account = settings.ACCOUNTING_CONFREG_ACCOUNT, accounting_object = sponsor.conference.accounting_object, autopaymentoptions = True ) return i
def create_sponsor_invoice(user, user_name, name, address, conference, level, sponsorid): invoicerows = [ ['%s %s sponsorship' % (conference, level), 1, level.levelcost], ] if conference.startdate < date.today() + timedelta(days=5): # If conference happens in the next 5 days, invoice is due immediately duedate = date.today() elif conference.startdate < date.today() + timedelta(days=30): # Less than 30 days before the conference, set the due date to # 5 days before the conference duedate = conference.startdate - timedelta(days=5) else: # More than 30 days before the conference, set the due date # to 30 days from now. duedate = datetime.now() + timedelta(days=30) manager = InvoiceManager() processor = invoicemodels.InvoiceProcessor.objects.get(processorname="confsponsor processor") i = manager.create_invoice( user, user.email, user_name, '%s\n%s' % (name, address), '%s sponsorship' % conference.conferencename, datetime.now(), duedate, invoicerows, processor = processor, processorid = sponsorid, bankinfo = True, accounting_account = settings.ACCOUNTING_CONFSPONSOR_ACCOUNT, accounting_object = conference.accounting_object, autopaymentoptions = False ) i.allowedmethods = level.paymentmethods.all() return i
def process_refund(notification): method = notification.rawnotification.paymentmethod pm = method.get_implementation() # Store the refund, and send an email! if notification.success: try: ts = TransactionStatus.objects.get( pspReference=notification.originalReference, paymentmethod=method) refund = Refund(notification=notification, transaction=ts, refund_amount=notification.amount) refund.save() urls = [ "https://ca-live.adyen.com/ca/ca/accounts/showTx.shtml?pspReference=%s&txType=Payment&accountKey=MerchantAccount.%s" % (notification.pspReference, notification.merchantAccountCode), ] # API generated refund? if notification.merchantReference.startswith( pm.config('merchantref_refund_prefix')): # API generated invoicerefundid = int(notification.merchantReference[ len(pm.config('merchantref_refund_prefix')):]) manager = InvoiceManager() manager.complete_refund( invoicerefundid, refund.refund_amount, 0, # we don't know the fee, it'll be generically booked pm.config('accounting_refunds'), pm.config('accounting_fee'), urls, method) else: # Generate an open accounting record for this refund. # We expect this happens so seldom that we can just deal with # manually finishing off the accounting records. accrows = [ (pm.config('accounting_refunds'), "Refund of %s (transaction %s) " % (ts.notes, ts.pspReference), -refund.refund_amount, None), ] send_simple_mail( settings.INVOICE_SENDER_EMAIL, pm.config('notification_receiver'), 'Adyen refund received', "A refund of %s%s for transaction %s was processed on %s\n\nNOTE! You must complete the accounting system entry manually as it was not API generated!!" % (settings.CURRENCY_ABBREV, notification.amount, method.internaldescription, notification.originalReference)) create_accounting_entry(date.today(), accrows, True, urls) except TransactionStatus.DoesNotExist: send_simple_mail( settings.INVOICE_SENDER_EMAIL, pm.config('notification_receiver'), 'Adyen refund received for nonexisting transaction', "A refund for %s was received on %s, but the transaction does not exist!\n\nYou probably want to investigate this!\n" % (notification.originalReference, method.internaldescription)) else: send_simple_mail( settings.INVOICE_SENDER_EMAIL, pm.config('notification_receiver'), 'Unsuccessful adyen refund received', "A refund for %s has failed on %s.\nThe reason given was:\n%s\n\nYou probably want to investigate this!\n" % (notification.merchantReference, method.internaldescription, notification.reason)) notification.confirmed = True notification.save()
ti.save() # Generate a simple accounting record, that will have to be # manually completed. accstr = "Paypal donation %s" % ti.paypaltransid accrows = [ (settings.ACCOUNTING_PAYPAL_INCOME_ACCOUNT, accstr, ti.amount-ti.fee, None), (settings.ACCOUNTING_PAYPAL_FEE_ACCOUNT, accstr, ti.fee, None), (settings.ACCOUNTING_DONATIONS_ACCOUNT, accstr, -ti.amount, None), ] create_accounting_entry(date.today(), accrows, True, urls) return render_to_response('paypal/noinvoice.html', { }, context_instance=RequestContext(request)) invoicemanager = InvoiceManager() (r,i,p) = invoicemanager.process_incoming_payment(ti.transtext, ti.amount, "Paypal id %s, from %s <%s>, auto" % (ti.paypaltransid, ti.sendername, ti.sender), ti.fee, settings.ACCOUNTING_PAYPAL_INCOME_ACCOUNT, settings.ACCOUNTING_PAYPAL_FEE_ACCOUNT, urls, payment_logger) if r == invoicemanager.RESULT_OK: # Matched it! ti.matched = True ti.matchinfo = 'Matched standard invoice (auto)' ti.save() # Now figure out where to return the user. This comes from the
def handle_method(self, method): pm = method.get_implementation() api = pm.get_api() for t in api.get_transactions(): # We will re-fetch most transactions, so only create them if they are not # already there. trans, created = TransferwiseTransaction.objects.get_or_create( paymentmethod=method, twreference=t['referenceNumber'], defaults={ 'datetime': api.parse_datetime(t['date']), 'amount': api.get_structured_amount(t['amount']), 'feeamount': api.get_structured_amount(t['totalFees']), 'transtype': t['details']['type'], 'paymentref': t['details']['paymentReference'][:200], 'fulldescription': t['details']['description'], }) if created: # Set optional fields trans.counterpart_name = t['details'].get('senderName', '') trans.counterpart_account = t['details'].get( 'senderAccount', '').replace(' ', '') # Weird stuff that sometimes shows up if trans.counterpart_account == 'Unknownbankaccount': trans.counterpart_account = '' if trans.counterpart_account: # If account is IBAN, then try to validate it! trans.counterpart_valid_iban = api.validate_iban( trans.counterpart_account) trans.save() # If this is a refund transaction, process it as such if trans.transtype == 'TRANSFER' and trans.paymentref.startswith( '{0} refund'.format(settings.ORG_SHORTNAME)): # Yes, this is one of our refunds. Can we find the corresponding transaction? m = re.match(r'^TRANSFER-(\d+)$', t['referenceNumber']) if not m: raise Exception( "Could not find TRANSFER info in transfer reference {0}" .format(t['referenceNumber'])) transferid = m.groups(1)[0] try: twrefund = TransferwiseRefund.objects.get( transferid=transferid) except TransferwiseRefund.DoesNotExist: print( "Could not find transferwise refund for id {0}, registering as manual bank transaction" .format(transferid)) register_bank_transaction(method, trans.id, trans.amount, trans.paymentref, trans.fulldescription, False) continue if twrefund.refundtransaction or twrefund.completedat: raise Exception( "Transferwise refund for id {0} has already been processed!" .format(transferid)) # Flag this one as done! twrefund.refundtransaction = trans twrefund.completedat = datetime.now() twrefund.save() invoicemanager = InvoiceManager() invoicemanager.complete_refund( twrefund.refundid, trans.amount + trans.feeamount, trans.feeamount, pm.config('bankaccount'), pm.config('feeaccount'), [], # urls method, ) elif trans.transtype == 'TRANSFER' and trans.paymentref.startswith( '{0} returned payment'.format(settings.ORG_SHORTNAME)): # Returned payment. Nothing much to do, but we create an accounting record # for it just to make things nice and clear. But first make sure we can # actually find the original transaction. try: po = TransferwisePayout.objects.get( reference=trans.paymentref) except TransferwisePayout.DoesNotExist: raise Exception( "Could not find transferwise payout object for {0}" .format(trans.paymentref)) po.completedat = datetime.now() po.completedtrans = trans po.save() m = re.match( r'^{0} returned payment (\d+)$'.format( settings.ORG_SHORTNAME), trans.paymentref) if not m: raise Exception( "Could not find returned transaction id in reference '{0}'" .format(trans.paymentref)) twtrans = TransferwiseTransaction.objects.get( pk=m.groups(1)[0]) if twtrans.amount != -trans.amount - trans.feeamount: raise Exception( "Original amount {0} does not match returned amount {1}" .format(twtrans.amount, -trans.amount - trans.feeamount)) accountingtxt = "TransferWise returned payment {0}".format( trans.twreference) accrows = [ (pm.config('bankaccount'), accountingtxt, trans.amount, None), (pm.config('feeaccount'), accountingtxt, trans.feeamount, None), (pm.config('bankaccount'), accountingtxt, -(trans.amount + trans.feeamount), None), ] create_accounting_entry(date.today(), accrows) elif trans.transtype == 'TRANSFER' and trans.paymentref.startswith( 'TW payout'): # Payout. Create an appropriate accounting record and a pending matcher. try: po = TransferwisePayout.objects.get( reference=trans.paymentref) except TransferwisePayout.DoesNotExist: raise Exception( "Could not find transferwise payout object for {0}" .format(trans.paymentref)) refno = int(trans.paymentref[len("TW payout "):]) if po.amount != -(trans.amount + trans.feeamount): raise Exception( "Transferwise payout {0} returned transaction with amount {1} instead of {2}" .format(refno, -(trans.amount + trans.feeamount), po.amount)) po.completedat = datetime.now() po.completedtrans = trans po.save() # Payout exists at TW, so proceed to generate records. If the receiving account # is a managed one, create a bank matcher. Otherwise just close the record # immediately. accrows = [ (pm.config('bankaccount'), trans.paymentref, trans.amount, None), (pm.config('feeaccount'), trans.paymentref, trans.feeamount, None), (pm.config('accounting_payout'), trans.paymentref, -(trans.amount + trans.feeamount), None), ] if is_managed_bank_account(pm.config('accounting_payout')): entry = create_accounting_entry( date.today(), accrows, True) register_pending_bank_matcher( pm.config('accounting_payout'), '.*TW.*payout.*{0}.*'.format(refno), -(trans.amount + trans.feeamount), entry) else: create_accounting_entry(date.today(), accrows) else: # Else register a pending bank transaction. This may immediately match an invoice # if it was an invoice payment, in which case the entire process will copmlete. register_bank_transaction(method, trans.id, trans.amount, trans.paymentref, trans.fulldescription, trans.counterpart_valid_iban)
def _invoice_payment(request, methodid, invoice, trailer): method = get_object_or_404(InvoicePaymentMethod, active=True, pk=methodid) pm = method.get_implementation() if trailer == 'return/': # This is a payment return URL, so we wait for the status to be posted. if invoice.ispaid: # Success, this invoice is paid! return HttpResponseRedirect(InvoiceManager().get_invoice_return_url(invoice)) # Else we wait for it to be. Return the pending page which will auto-refresh itself. # We sneakily use the "pspReference" field and put the invoice id in it, because that will never # conflict with an actual Adyen pspReference. status, created = ReturnAuthorizationStatus.objects.get_or_create(pspReference='INVOICE{}'.format(invoice.id)) status.seencount += 1 status.save() return render(request, 'adyen/authorized.html', { 'refresh': 3**status.seencount, 'returnurl': InvoiceManager().get_invoice_return_url(invoice), }) if trailer == 'iban/': methods = ['bankTransfer_IBAN'] else: methods = ['card'] # Not the return handler, so use the Adyen checkout API to build a payment link. p = { 'reference': '{}{}'.format(pm.config('merchantref_prefix'), invoice.id), 'amount': { 'value': int(invoice.total_amount * Decimal(100.0)), 'currency': 'EUR', }, 'description': invoice.invoicestr, 'merchantAccount': pm.config('merchantaccount'), 'allowedPaymentMethods': methods, 'returnUrl': '{}/invoices/adyenpayment/{}/{}/{}/return/'.format(settings.SITEBASE, methodid, invoice.id, invoice.recipient_secret), } try: r = requests.post( '{}/v68/paymentLinks'.format(pm.config('checkoutbaseurl').rstrip('/')), json=p, headers={ 'x-api-key': pm.config('ws_apikey'), }, timeout=10, ) if r.status_code != 201: AdyenLog(pspReference='', message='Status code {} when trying to create a payment link. Response: {}'.format(r.status_code, r.text), error=True, paymentmethod=method).save() return HttpResponse('Failed to create payment link. Please try again later.') j = r.json() AdyenLog(pspReference='', message='Created payment link {} for invoice {}'.format(j['id'], invoice.id), error=False, paymentmethod=method).save() # Then redirect the user to the payment link we received return HttpResponseRedirect(j['url']) except requests.exceptions.ReadTimeout: AdyenLog(pspReference='', message='timeout when trying to create a payment link', error=True, paymentmethod=method).save() return HttpResponse('Failed to create payment link. Please try again later.') except Exception as e: AdyenLog(pspReference='', message='Exception when trying to create a payment link:{}'.format(e), error=True, paymentmethod=method).save() return HttpResponse('Failed to create payment link. Please try again later.')
def paypal_return_handler(request, methodid): tx = 'UNKNOWN' method = get_object_or_404(InvoicePaymentMethod, pk=int(methodid), active=True) pm = method.get_implementation() # Custom error return that can get to the request context def paypal_error(reason): return render(request, 'paypal/error.html', { 'reason': reason, }) # Logger for the invoice processing - we store it in the genereal # paypal logs def payment_logger(msg): ErrorLog( timestamp=timezone.now(), sent=False, message='Paypal automatch for %s: %s' % (tx, msg), paymentmethod=method, ).save() # Now for the main handler # Handle a paypal PDT return if 'tx' not in request.GET: return paypal_error('Transaction id not received from paypal') tx = request.GET['tx'] # We have a transaction id. First we check if we already have it # in the database. # We only store transactions with status paid, so if it's in there, # then it's already paid, and what's happening here is a replay # (either by mistake or intentional). So we don't redirect the user # at this point, we just give an error message. try: ti = TransactionInfo.objects.get(paypaltransid=tx) return HttpResponseForbidden( 'This transaction has already been processed') except TransactionInfo.DoesNotExist: pass # We haven't stored the status of this transaction. It either means # this is the first load, or that we have only seen pending state on # it before. Thus, we need to post back to paypal to figure out the # current status. try: params = { 'cmd': '_notify-synch', 'tx': tx, 'at': pm.config('pdt_token'), } resp = requests.post(pm.get_baseurl(), data=params) if resp.status_code != 200: raise Exception("status code {0}".format(resp.status_code)) r = resp.text except Exception as ex: # Failed to talk to paypal somehow. It should be ok to retry. return paypal_error('Failed to verify status with paypal: %s' % ex) # First line of paypal response contains SUCCESS if we got a valid # response (which might *not* mean it's actually a payment!) lines = r.split("\n") if lines[0] != 'SUCCESS': return paypal_error('Received an error from paypal.') # Drop the SUCCESS line lines = lines[1:] # The rest of the response is urlencoded key/value pairs d = dict([unquote_plus(line).split('=') for line in lines if line != '']) # Validate things that should never be wrong try: if d['txn_id'] != tx: return paypal_error('Received invalid transaction id from paypal') if d['txn_type'] != 'web_accept': return paypal_error( 'Received transaction type %s which is unknown by this system!' % d['txn_type']) if d['business'] != pm.config('email'): return paypal_error( 'Received payment for %s which is not the correct recipient!' % d['business']) if d['mc_currency'] != settings.CURRENCY_ABBREV: return paypal_error( 'Received payment in %s, not %s. We cannot currently process this automatically.' % (d['mc_currency'], settings.CURRENCY_ABBREV)) except KeyError as k: return paypal_error('Mandatory field %s is missing from paypal data!', k) # Now let's find the state of the payment if 'payment_status' not in d: return paypal_error('Payment status not received from paypal!') if d['payment_status'] == 'Completed': # Payment is completed. Create a paypal transaction info # object for it, and then try to match it to an invoice. # Double-check if it is already added. We did check this furter # up, but it seems it can sometimes be called more than once # asynchronously, due to the check with paypal taking too # long. if TransactionInfo.objects.filter(paypaltransid=tx).exists(): return HttpResponse("Transaction already processed", content_type='text/plain') # Paypal seems to randomly change which field actually contains # the transaction title. if d.get('transaction_subject', ''): transtext = d['transaction_subject'] else: transtext = d['item_name'] ti = TransactionInfo(paypaltransid=tx, timestamp=timezone.now(), paymentmethod=method, sender=d['payer_email'], sendername=d['first_name'] + ' ' + d['last_name'], amount=Decimal(d['mc_gross']), fee=Decimal(d['mc_fee']), transtext=transtext, matched=False) ti.save() # Generate URLs that link back to paypal in a way that we can use # from the accounting system. Note that this is an undocumented # URL format for paypal, so it may stop working at some point in # the future. urls = [ "%s?cmd=_view-a-trans&id=%s" % ( pm.get_baseurl(), ti.paypaltransid, ), ] # Separate out donations made through our website if ti.transtext == pm.config('donation_text'): ti.matched = True ti.matchinfo = 'Donation, automatically matched' ti.save() # Generate a simple accounting record, that will have to be # manually completed. accstr = "Paypal donation %s" % ti.paypaltransid accrows = [ (pm.config('accounting_income'), accstr, ti.amount - ti.fee, None), (pm.config('accounting_fee'), accstr, ti.fee, None), (settings.ACCOUNTING_DONATIONS_ACCOUNT, accstr, -ti.amount, None), ] create_accounting_entry(accrows, True, urls) return render(request, 'paypal/noinvoice.html', {}) invoicemanager = InvoiceManager() (r, i, p) = invoicemanager.process_incoming_payment( ti.transtext, ti.amount, "Paypal id %s, from %s <%s>, auto" % (ti.paypaltransid, ti.sendername, ti.sender), ti.fee, pm.config('accounting_income'), pm.config('accounting_fee'), urls, payment_logger, method, ) if r == invoicemanager.RESULT_OK: # Matched it! ti.matched = True ti.matchinfo = 'Matched standard invoice (auto)' ti.save() # Now figure out where to return the user. This comes from the # invoice processor, assuming we have one if p: url = p.get_return_url(i) else: # No processor, so redirect the user back to the basic # invoice page. if i.recipient_user: # Registered to a specific user, so request that users # login on redirect url = "%s/invoices/%s/" % (settings.SITEBASE, i.pk) else: # No user account registered, so send back to the secret # url version url = "%s/invoices/%s/%s/" % (settings.SITEBASE, i.pk, i.recipient_secret) return render(request, 'paypal/complete.html', { 'invoice': i, 'url': url, }) else: # Did not match an invoice anywhere! # We'll leave the transaction in the paypal transaction # list, where it will generate an alert in the nightly mail. return render(request, 'paypal/noinvoice.html', {}) # For a pending payment, we set ourselves up with a redirect loop if d['payment_status'] == 'Pending': try: pending_reason = d['pending_reason'] except Exception as e: pending_reason = 'no reason given' return render(request, 'paypal/pending.html', { 'reason': pending_reason, }) return paypal_error('Unknown payment status %s.' % d['payment_status'])
def handle(self, *args, **options): invoicemanager = InvoiceManager() # There may be multiple accounts, so loop over them for method in InvoicePaymentMethod.objects.filter( active=True, classname='postgresqleu.util.payment.paypal.Paypal'): pm = method.get_implementation() translist = TransactionInfo.objects.filter( matched=False, paymentmethod=method).order_by('timestamp') for trans in translist: # URLs for linkback to paypal urls = [ "%s?cmd=_view-a-trans&id=%s" % ( pm.get_baseurl(), trans.paypaltransid, ), ] # Manual handling of some record types # Record type: donation if trans.transtext == pm.config('donation_text'): trans.setmatched( 'Donation, automatically matched by script') # Generate a simple accounting record, that will have to be # manually completed. accstr = "Paypal donation %s" % trans.paypaltransid accrows = [ (pm.config('accounting_income'), accstr, trans.amount - trans.fee, None), (pm.config('accounting_fee'), accstr, trans.fee, None), (settings.ACCOUNTING_DONATIONS_ACCOUNT, accstr, -trans.amount, None), ] create_accounting_entry(trans.timestamp.date(), accrows, True, urls) continue # Record type: payment, but with no notice (auto-generated elsewhere, the text is # hard-coded in paypal_fetch.py if trans.transtext == "Paypal payment with empty note" or trans.transtext == "Recurring paypal payment without note": trans.setmatched( 'Empty payment description, leaving for operator') accstr = "Unlabeled paypal payment from {0}".format( trans.sender) accrows = [ (pm.config('accounting_income'), accstr, trans.amount - trans.fee, None), ] if trans.fee: accrows.append((pm.config('accounting_fee'), accstr, trans.fee, None), ) create_accounting_entry(trans.timestamp.date(), accrows, True, urls) continue # Record type: transfer if trans.amount < 0 and trans.transtext == 'Transfer from Paypal to bank': trans.setmatched( 'Bank transfer, automatically matched by script') # There are no fees on the transfer, and the amount is already # "reversed" and will automatically become a credit entry. accstr = 'Transfer from Paypal to bank' accrows = [ (pm.config('accounting_income'), accstr, trans.amount, None), (pm.config('accounting_transfer'), accstr, -trans.amount, None), ] entry = create_accounting_entry(trans.timestamp.date(), accrows, True, urls) if is_managed_bank_account( pm.config('accounting_transfer')): register_pending_bank_matcher( pm.config('accounting_transfer'), '.*PAYPAL.*', -trans.amount, entry) continue textstart = 'Refund of Paypal payment: {0} refund '.format( settings.ORG_SHORTNAME) if trans.amount < 0 and trans.transtext.startswith(textstart): trans.setmatched('Matched API initiated refund') # API initiated refund, so we should be able to match it invoicemanager.complete_refund( trans.transtext[len(textstart):], -trans.amount, -trans.fee, pm.config('accounting_income'), pm.config('accounting_fee'), urls, method, ) # Accounting record is created by invoice manager continue # Record type: outgoing payment (or manual refund) if trans.amount < 0: trans.setmatched( 'Outgoing payment or manual refund, automatically matched by script' ) # Refunds typically have a fee (a reversed fee), whereas pure # payments don't have one. We don't make a difference of them # though - we leave the record open for manual verification accrows = [ (pm.config('accounting_income'), trans.transtext[:200], trans.amount - trans.fee, None), ] if trans.fee != 0: accrows.append( (pm.config('accounting_fee'), trans.transtext[:200], trans.fee, None), ) create_accounting_entry(trans.timestamp.date(), accrows, True, urls) continue # Otherwise, it's an incoming payment. In this case, we try to # match it to an invoice. # Log things to the db def payment_logger(msg): # Write the log output to somewhere interesting! ErrorLog( timestamp=datetime.now(), sent=False, message='Paypal %s by %s (%s) on %s: %s' % (trans.paypaltransid, trans.sender, trans.sendername, trans.timestamp, msg), paymentmethod=method, ).save() (r, i, p) = invoicemanager.process_incoming_payment( trans.transtext, trans.amount, "Paypal id %s, from %s <%s>" % (trans.paypaltransid, trans.sendername, trans.sender), trans.fee, pm.config('accounting_income'), pm.config('accounting_fee'), urls, payment_logger, method, ) if r == invoicemanager.RESULT_OK: trans.setmatched('Matched standard invoice') elif r == invoicemanager.RESULT_NOTMATCHED: # Could not match this to our pattern of transaction texts. Treat it # the same way as we would treat an empty one -- create an open accounting # entry. trans.setmatched( 'Could not match transaction text, leaving for operator' ) accstr = "Paypal payment '{0}' from {1}".format( trans.transtext, trans.sender) accrows = [ (pm.config('accounting_income'), accstr, trans.amount - trans.fee, None), ] if trans.fee: accrows.append((pm.config('accounting_fee'), accstr, trans.fee, None), ) create_accounting_entry(trans.timestamp.date(), accrows, True, urls) else: # Logging is done by the invoice manager callback pass
def run(): invoicemanager = InvoiceManager() translist = TransactionInfo.objects.filter(matched=False).order_by('timestamp') for trans in translist: # URLs for linkback to paypal urls = ["https://www.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s" % trans.paypaltransid,] # Manual handling of some record types # Record type: donation if trans.transtext == "PostgreSQL Europe donation": trans.setmatched('Donation, automatically matched by script') # Generate a simple accounting record, that will have to be # manually completed. accstr = "Paypal donation %s" % trans.paypaltransid accrows = [ (settings.ACCOUNTING_PAYPAL_INCOME_ACCOUNT, accstr, trans.amount-trans.fee, None), (settings.ACCOUNTING_PAYPAL_FEE_ACCOUNT, accstr, trans.fee, None), (settings.ACCOUNTING_DONATIONS_ACCOUNT, accstr, -trans.amount, None), ] create_accounting_entry(trans.timestamp.date(), accrows, True, urls) continue # Record type: transfer if trans.amount < 0 and trans.transtext == 'Transfer from Paypal to bank': trans.setmatched('Bank transfer, automatically matched by script') # There are no fees on the transfer, and the amount is already # "reversed" and will automatically become a credit entry. accstr = 'Transfer from Paypal to bank' accrows = [ (settings.ACCOUNTING_PAYPAL_INCOME_ACCOUNT, accstr, trans.amount, None), (settings.ACCOUNTING_PAYPAL_TRANSFER_ACCOUNT, accstr, -trans.amount, None), ] create_accounting_entry(trans.timestamp.date(), accrows, True, urls) continue # Record type: payment (or refund) if trans.amount < 0: trans.setmatched('Payment or refund, automatically matched by script') # Refunds typically have a fee (a reversed fee), whereas pure # payments don't have one. We don't make a difference of them # though - we lave the record open for manual verification accrows = [ (settings.ACCOUNTING_PAYPAL_INCOME_ACCOUNT, trans.transtext, trans.amount - trans.fee, None), ] if trans.fee <> 0: accrows.append((settings.ACCOUNTING_PAYPAL_FEE_ACCOUNT, trans.transtext, trans.fee, None),) create_accounting_entry(trans.timestamp.date(), accrows, True, urls) continue # Otherwise, it's an incoming payment. In this case, we try to # match it to an invoice. # Log things to the db def payment_logger(msg): # Write the log output to somewhere interesting! ErrorLog(timestamp=datetime.now(), sent=False, message='Paypal %s by %s (%s) on %s: %s' % ( trans.paypaltransid, trans.sender, trans.sendername, trans.timestamp, msg )).save() (r,i,p) = invoicemanager.process_incoming_payment(trans.transtext, trans.amount, "Paypal id %s, from %s <%s>" % (trans.paypaltransid, trans.sendername, trans.sender), trans.fee, settings.ACCOUNTING_PAYPAL_INCOME_ACCOUNT, settings.ACCOUNTING_PAYPAL_FEE_ACCOUNT, urls, payment_logger) if r == invoicemanager.RESULT_OK: trans.setmatched('Matched standard invoice') else: # Logging is done by the invoice manager callback pass
def handle(self, *args, **options): # We're always going to process all conferences, since most will not have any # open discount codes. filt = Q(sponsor__isnull=False, is_invoiced=False) & ( Q(validuntil__lte=today_global()) | Q(num_uses__gte=F('maxuses'))) codes = DiscountCode.objects.annotate( num_uses=Count('registrations')).filter(filt) for code in codes: # Either the code has expired, or it is fully used by now. Time to generate the invoice. We'll also # send an email to the sponsor (and the admins) to inform them of what's happening. # The invoice will be a one-off one, we don't need a registered manager for it since the # discounts have already been given out. if code.count == 0: # In case there is not a single user, we just notify the user of this and set it to # invoiced in the system so we don't try again. code.is_invoiced = True code.save() send_conference_sponsor_notification( code.conference, "[{0}] Discount code expired".format(code.conference), "Discount code {0} has expired without any uses.".format( code.code), ) for manager in code.sponsor.managers.all(): send_conference_mail( code.conference, manager.email, "Discount code {0} expired".format(code.code), 'confsponsor/mail/discount_expired.txt', { 'code': code, 'sponsor': code.sponsor, 'conference': code.conference, }, sender=code.conference.sponsoraddr, receivername='{0} {1}'.format(manager.first_name, manager.last_name)) else: # At least one use, so we generate the invoice invoicerows = [] for r in code.registrations.all(): if code.discountamount: # Fixed amount discount. Always apply discountvalue = code.discountamount else: # Percentage discount, so we need to calculate it. Ordered discount codes will # only support a registration-only style discount code, so only count it # against that. discountvalue = r.regtype.cost * code.discountpercentage / 100 invoicerows.append([ 'Attendee "{0}"'.format(r.fullname), 1, discountvalue, r.conference.vat_registrations ]) # All invoices are always due immediately manager = InvoiceManager() code.invoice = manager.create_invoice( code.sponsor_rep, code.sponsor_rep.email, "{0} {1}".format(code.sponsor_rep.first_name, code.sponsor_rep.last_name), '%s\n%s' % (code.sponsor.name, code.sponsor.invoiceaddr), '{0} discount code {1}'.format(code.conference, code.code), timezone.now(), timezone.now() + timedelta(days=1), invoicerows, accounting_account=settings.ACCOUNTING_CONFREG_ACCOUNT, accounting_object=code.conference.accounting_object, paymentmethods=code.conference.paymentmethods.all(), ) code.invoice.save() code.is_invoiced = True code.save() wrapper = InvoiceWrapper(code.invoice) wrapper.email_invoice() # Now also fire off emails, both to the admins and to all the managers of the sponsor # (so they know where the invoice was sent). send_conference_sponsor_notification( code.conference, "[{0}] Discount code {1} has been invoiced".format( code.conference, code.code), "The discount code {0} has been closed,\nand an invoice has been sent to {1}.\n\nA total of {2} registrations used this code, and the total amount was {3}.\n" .format( code.code, code.sponsor, len(invoicerows), code.invoice.total_amount, ), ) for manager in code.sponsor.managers.all(): send_conference_mail( code.conference, manager.email, "Discount code {0} has been invoiced".format( code.code), 'confsponsor/mail/discount_invoiced.txt', { 'code': code, 'conference': code.conference, 'sponsor': code.sponsor, 'invoice': code.invoice, 'curr': settings.CURRENCY_ABBREV, 'expired_time': code.validuntil < today_global(), }, sender=code.conference.sponsoraddr, receivername='{0} {1}'.format(manager.first_name, manager.last_name))
def home(request): try: member = Member.objects.get(user=request.user) registration_complete = True # We have a batch job that expires members, but do it here as well to make sure # the web is up to date with information if necessary. if member.paiduntil and member.paiduntil < today_global(): MemberLog(member=member, timestamp=timezone.now(), message="Membership expired").save() member.membersince = None member.paiduntil = None member.save() except Member.DoesNotExist: # No record yet, so we create one. Base the information on whatever we # have already. member = Member(user=request.user, fullname="{0} {1}".format(request.user.first_name, request.user.last_name)) registration_complete = False cfg = get_config() if request.method == "POST": if request.POST["submit"] == "Generate invoice": # Generate an invoice for the user if member.activeinvoice: raise Exception("This should not happen - generating invoice when one already exists!") manager = InvoiceManager() processor = InvoiceProcessor.objects.get(processorname="membership processor") invoicerows = [('%s - %s years membership - %s' % (settings.ORG_NAME, cfg.membership_years, request.user.email), 1, cfg.membership_cost, None), ] member.activeinvoice = manager.create_invoice( request.user, request.user.email, request.user.first_name + ' ' + request.user.last_name, '', # We don't have an address '%s membership for %s' % (settings.ORG_NAME, request.user.email), timezone.now(), timezone.now(), invoicerows, processor=processor, processorid=member.pk, canceltime=timezone.now() + timedelta(days=7), accounting_account=settings.ACCOUNTING_MEMBERSHIP_ACCOUNT, paymentmethods=cfg.paymentmethods.all(), ) member.activeinvoice.save() member.save() # We'll redirect back to the same page, so make sure # someone doing say a hard refresh on the page doesn't # cause weird things to happen. return HttpResponseRedirect('/membership/') form = MemberForm(data=request.POST, instance=member) if form.is_valid(): member = form.save(commit=False) member.user = request.user member.save() if not registration_complete: MemberLog(member=member, timestamp=timezone.now(), message="Registration received, awaiting payment").save() registration_complete = True # So we show the payment info! elif form.has_changed(): # Figure out what changed MemberLog(member=member, timestamp=timezone.now(), message="Modified registration data for field(s): %s" % (", ".join(form.changed_data)), ).save() return HttpResponseRedirect(".") else: form = MemberForm(instance=member) logdata = MemberLog.objects.filter(member=member).order_by('-timestamp')[:30] return render(request, 'membership/index.html', { 'form': form, 'member': member, 'invoice': InvoicePresentationWrapper(member.activeinvoice, "%s/membership/" % settings.SITEBASE), 'registration_complete': registration_complete, 'logdata': logdata, 'amount': cfg.membership_cost, 'cancelurl': '/account/', })
def home(request): try: member = Member.objects.get(user=request.user) registration_complete = True # We have a batch job that expires members, but do it here as well to make sure # the web is up to date with information if necessary. if member.paiduntil and member.paiduntil < date.today(): MemberLog(member=member, timestamp=datetime.now(), message="Membership expired").save() member.membersince = None member.paiduntil = None member.save() except Member.DoesNotExist: # No record yet, so we create one. Base the information on whatever we # have already. member = Member(user=request.user, fullname=request.user.first_name) registration_complete = False if request.method == "POST": form = MemberForm(data=request.POST, instance=member) if form.is_valid(): member = form.save(commit=False) member.user = request.user member.save() if not registration_complete: MemberLog(member=member, timestamp=datetime.now(), message="Registration received, awaiting payment").save() registration_complete = True # So we show the payment info! elif form.has_changed(): # Figure out what changed MemberLog(member=member, timestamp=datetime.now(), message="Modified registration data for field(s): %s" % (", ".join(form._changed_data)), ).save() if request.POST["submit"] == "Generate invoice": # Generate an invoice for the user if member.activeinvoice: raise Exception("This should not happen - generating invoice when one already exists!") manager = InvoiceManager() processor = InvoiceProcessor.objects.get(processorname="membership processor") invoicerows = [('PostgreSQL Europe - 2 years membership - %s' % request.user.email, 1, 10),] member.activeinvoice = manager.create_invoice( request.user, request.user.email, request.user.first_name + ' ' + request.user.last_name, '', # We don't have an address 'PostgreSQL Europe membership for %s'% request.user.email, datetime.now(), datetime.now(), invoicerows, processor = processor, processorid = member.pk, bankinfo = False, accounting_account = settings.ACCOUNTING_MEMBERSHIP_ACCOUNT ) member.activeinvoice.save() member.save() # We'll redirect back to the same page, so make sure # someone doing say a hard refresh on the page doesn't # cause weird things to happen. return HttpResponseRedirect('/membership/') else: form = MemberForm(instance=member) logdata = MemberLog.objects.filter(member=member).order_by('-timestamp')[:30] return render_to_response('membership/index.html', { 'form': form, 'member': member, 'invoice': InvoicePresentationWrapper(member.activeinvoice, "%s/membership/" % settings.SITEBASE_SSL), 'registration_complete': registration_complete, 'logdata': logdata, 'amount': 10, # price for two years }, context_instance=RequestContext(request))
def process_authorization(notification): if notification.success: # This is a successful notification, so flag this invoice # as paid. We also create a TransactionStatus for it, so that # can validate that it goes from authorized->captured. trans = TransactionStatus(pspReference=notification.pspReference, notification=notification, authorizedat=datetime.now(), amount=notification.amount, method=notification.paymentMethod, notes=notification.merchantReference, capturedat=None) trans.save() # Generate urls pointing back to this entry in the Adyen online # system, for inclusion in accounting records. urls = ["https://ca-live.adyen.com/ca/ca/accounts/showTx.shtml?pspReference=%s&txType=Payment&accountKey=MerchantAccount.%s" % (notification.pspReference, notification.merchantAccountCode),] # We can receive authorizations on non-primary Adyen merchant # accounts. This happens for example with payments from POS # terminals. In those cases, just send an email, and don't # try to match it to any invoices. # We still store and track the transaction. if notification.merchantAccountCode != settings.ADYEN_MERCHANTACCOUNT: send_simple_mail(settings.INVOICE_SENDER_EMAIL, settings.ADYEN_NOTIFICATION_RECEIVER, 'Manual Adyen payment authorized', "An Adyen payment of %s%s was authorized on the Adyen platform.\nThis payment was not from the automated system, it was manually authorized, probably from a POS terminal.\nReference: %s\nAdyen reference: %s\nMerchant account: %s\n" % (settings.CURRENCY_ABBREV, notification.amount, notification.merchantReference, notification.pspReference, notification.merchantAccountCode)) notification.confirmed = True notification.save() # For manual payments, we can only create an open-ended entry # in the accounting accstr = "Manual Adyen payment: %s (%s)" % (notification.merchantReference, notification.pspReference) accrows = [ (settings.ACCOUNTING_ADYEN_AUTHORIZED_ACCOUNT, accstr, trans.amount, None), ] create_accounting_entry(date.today(), accrows, True, urls) return # Process a payment on the primary account manager = InvoiceManager() try: # Figure out the invoiceid if not notification.merchantReference.startswith(settings.ADYEN_MERCHANTREF_PREFIX): raise AdyenProcessingException('Merchant reference does not start with %s' % settings.ADYEN_MERCHANTREF_PREFIX) invoiceid = int(notification.merchantReference[len(settings.ADYEN_MERCHANTREF_PREFIX):]) # Get the actual invoice try: invoice = Invoice.objects.get(pk=invoiceid) except Invoice.DoesNotExist: raise AdyenProcessingException('Invoice with id %s does not exist' % invoiceid) def invoice_logger(msg): raise AdyenProcessingException('Invoice processing failed: %s', msg) manager.process_incoming_payment_for_invoice(invoice, notification.amount, 'Adyen id %s' % notification.pspReference, 0, settings.ACCOUNTING_ADYEN_AUTHORIZED_ACCOUNT, 0, urls, invoice_logger) if invoice.accounting_object: # Store the accounting object so we can properly tag the # fee for it when we process the settlement (since we don't # actually know the fee yet) trans.accounting_object = invoice.accounting_object trans.save() # If nothing went wrong, then this invoice is now fully # flagged as paid in the system. send_simple_mail(settings.INVOICE_SENDER_EMAIL, settings.ADYEN_NOTIFICATION_RECEIVER, 'Adyen payment authorized', "An Adyen payment of %s%s with reference %s was authorized on the Adyen platform.\nInvoice: %s\nRecipient name: %s\nRecipient user: %s\nAdyen reference: %s\n" % (settings.CURRENCY_ABBREV, notification.amount, notification.merchantReference, invoice.title, invoice.recipient_name, invoice.recipient_email, notification.pspReference)) except AdyenProcessingException, ex: # Generate an email telling us about this exception! send_simple_mail(settings.INVOICE_SENDER_EMAIL, settings.ADYEN_NOTIFICATION_RECEIVER, 'Exception occured processing Adyen notification', "An exception occured processing the notification for %s:\n\n%s\n" % ( notification.merchantReference, ex) ) # We have stored the notification already, but we want # to make sure it's not *confirmed*. That way it'll keep # bugging the user. So, return here instead of confirming # it. return
def payment_post(request): nonce = request.POST['payment_method_nonce'] invoice = get_object_or_404(Invoice, pk=get_int_or_error(request.POST, 'invoice'), deleted=False, finalized=True) method = get_object_or_404(InvoicePaymentMethod, pk=get_int_or_error(request.POST, 'method'), active=True) pm = method.get_implementation() if invoice.processor: manager = InvoiceManager() processor = manager.get_invoice_processor(invoice) returnurl = processor.get_return_url(invoice) else: if invoice.recipient_user: returnurl = "%s/invoices/%s/" % (settings.SITEBASE, invoice.pk) else: returnurl = "%s/invoices/%s/%s/" % (settings.SITEBASE, invoice.pk, invoice.recipient_secret) # Generate the transaction result = pm.braintree_sale({ 'amount': '{0}'.format(invoice.total_amount), 'order_id': '#{0}'.format(invoice.pk), 'payment_method_nonce': nonce, 'merchant_account_id': pm.config('merchantacctid'), 'options': { 'submit_for_settlement': True, } }) trans = result.transaction if result.is_success: # Successful transaction. Store it for later processing. At authorization, we proceed to # flag the payment as done. BraintreeLog(transid=trans.id, message='Received successful result for {0}'.format(trans.id), paymentmethod=method).save() if trans.currency_iso_code != settings.CURRENCY_ISO: BraintreeLog(transid=trans.id, error=True, message='Invalid currency {0}, should be {1}'.format(trans.currency_iso_code, settings.CURRENCY_ISO), paymentmethod=method).save() send_simple_mail(settings.INVOICE_SENDER_EMAIL, pm.config('notification_receiver'), 'Invalid currency received in Braintree payment', 'Transaction {0} paid in {1}, should be {2}.'.format(trans.id, trans.currency_iso_code, settings.CURRENCY_ISO)) # We'll just throw the "processing error" page, and have # the operator deal with the complaints as this is a # should-never-happen scenario. return render(request, 'braintreepayment/processing_error.html') with transaction.atomic(): # Flag the invoice as paid manager = InvoiceManager() try: def invoice_logger(msg): raise BraintreeProcessingException('Invoice processing failed: %s'.format(msg)) manager.process_incoming_payment_for_invoice(invoice, trans.amount, 'Braintree id {0}'.format(trans.id), 0, pm.config('accounting_authorized'), 0, [], invoice_logger, method, ) except BraintreeProcessingException as ex: send_simple_mail(settings.INVOICE_SENDER_EMAIL, pm.config('notification_receiver'), 'Exception occurred processing Braintree result', "An exception occured processing the payment result for {0}:\n\n{1}\n".format(trans.id, ex)) return render(request, 'braintreepayment/processing_error.html') # Create a braintree transaction - so we can update it later when the transaction settles bt = BraintreeTransaction(transid=trans.id, authorizedat=timezone.now(), amount=trans.amount, method=trans.credit_card['card_type'], paymentmethod=method) if invoice.accounting_object: bt.accounting_object = invoice.accounting_object bt.save() send_simple_mail(settings.INVOICE_SENDER_EMAIL, pm.config('notification_receiver'), 'Braintree payment authorized', "A payment of %s%s with reference %s was authorized on the Braintree platform for %s.\nInvoice: %s\nRecipient name: %s\nRecipient user: %s\nBraintree reference: %s\n" % ( settings.CURRENCY_ABBREV, trans.amount, trans.id, method.internaldescription, invoice.title, invoice.recipient_name, invoice.recipient_email, trans.id)) return HttpResponseRedirect(returnurl) else: if not trans: reason = "Internal error" elif trans.status == 'processor_declined': reason = "Processor declined: {0}/{1}".format(trans.processor_response_code, trans.processor_response_text) elif trans.status == 'gateway_rejected': reason = "Gateway rejected: {0}".format(trans.gateway_rejection_reason) else: reason = "unknown" BraintreeLog(transid=trans and trans.id or "UNKNOWN", message='Received FAILED result for {0}'.format(trans and trans.id or "UNKNOWN"), error=True, paymentmethod=method).save() return render(request, 'braintreepayment/payment_failed.html', { 'invoice': invoice, 'reason': reason, 'url': returnurl, })
def process_one_account(self, method): pm = method.get_implementation() trustly = Trustly(pm) manager = InvoiceManager() refunds = InvoiceRefund.objects.filter(completed__isnull=True, invoice__paidusing=method) for r in refunds: # Find the matching Trustly transaction trustlytransactionlist = list( TrustlyTransaction.objects.filter(invoiceid=r.invoice.pk, paymentmethod=method)) if len(trustlytransactionlist) == 0: raise CommandError( "Could not find trustly transaction for invoice {0}". format(r.invoice.pk)) elif len(trustlytransactionlist) != 1: raise CommandError( "Found {0} trustly transactions for invoice {1}!".format( len(trustlytransactionlist), r.invoice.pk)) trustlytrans = trustlytransactionlist[0] w = trustly.getwithdrawal(trustlytrans.orderid) if not w: # No refund yet continue if w['transferstate'] != 'CONFIRMED': # Still pending continue if w['currency'] != settings.CURRENCY_ABBREV: # If somebody paid in a different currency (and Trustly converted it for us), # the withdrawal entry is specified in the original currency, which is more than # a little annoying. To deal with it, attempt to fetch the ledger for the day # and if we can find it there, use the amount from that one. day = dateutil.parser.parse(w['datestamp']).date() ledgerrows = trustly.getledgerforday(day) for lr in ledgerrows: if int(lr['orderid']) == trustlytrans.orderid and lr[ 'accountname'] == 'BANK_WITHDRAWAL_QUEUED': # We found the corresponding accounting row. So we take the amount from # this and convert the difference to what we expeced into the fee. This # can end up being a negative fee, but it should be small enough that # it's not a real problem. fees = ( r.fullamount + Decimal(lr['amount']).quantize(Decimal('0.01'))) TrustlyLog( message= "Refund for order {0}, invoice {1}, was made as {2} {3} instead of {4} {5}. Using ledger mapped to {6} {7} with difference of {8} {9} booked as fees" .format( trustlytrans.orderid, r.invoice.pk, Decimal(w['amount']), w['currency'], r.fullamount, settings.CURRENCY_ABBREV, Decimal(lr['amount']).quantize( Decimal('0.01')), settings.CURRENCY_ABBREV, fees, settings.CURRENCY_ABBREV, ), error=False, paymentmethod=method, ).save() break else: # Unable to find the refund in the ledger. This could be a matter of timing, # so yell about it but try agian. raise CommandError( "Trustly refund for invoice {0} was made in {1} instead of {2}, but could not be found in ledger." .format(r.invoice.pk, w['currency'], settings.CURRENCY_ABBREV)) else: # Currency is correct, so check that the refunded amount is the same as # the one we expected. if Decimal(w['amount']) != r.fullamount: raise CommandError( "Mismatch in amount on Trustly refund for invoice {0} ({1} vs {2})" .format(r.invoice.pk, Decimal(w['amount']), r.fullamount)) fees = 0 # Ok, things look good! TrustlyLog( message="Refund for order {0}, invoice {1}, completed".format( trustlytrans.orderid, r.invoice.pk), error=False, paymentmethod=method).save() manager.complete_refund(r.id, r.fullamount, fees, pm.config('accounting_income'), pm.config('accounting_fee'), [], method)
def process_authorization(notification): method = notification.rawnotification.paymentmethod pm = method.get_implementation() if notification.success: # This is a successful notification, so flag this invoice # as paid. We also create a TransactionStatus for it, so that # can validate that it goes from authorized->captured. trans = TransactionStatus(pspReference=notification.pspReference, notification=notification, authorizedat=datetime.now(), amount=notification.amount, method=notification.paymentMethod, notes=notification.merchantReference, capturedat=None, paymentmethod=method) trans.save() # Generate urls pointing back to this entry in the Adyen online # system, for inclusion in accounting records. urls = [ "https://ca-live.adyen.com/ca/ca/accounts/showTx.shtml?pspReference=%s&txType=Payment&accountKey=MerchantAccount.%s" % (notification.pspReference, notification.merchantAccountCode), ] # We can receive authorizations on non-primary Adyen merchant # accounts. This happens for example with payments from POS # terminals. In those cases, just send an email, and don't # try to match it to any invoices. # We still store and track the transaction. if notification.merchantAccountCode != pm.config('merchantaccount'): send_simple_mail( settings.INVOICE_SENDER_EMAIL, pm.config('notification_receiver'), 'Manual Adyen payment authorized', "An Adyen payment of %s%s was authorized on the Adyen platform for %s.\nThis payment was not from the automated system, it was manually authorized, probably from a POS terminal.\nReference: %s\nAdyen reference: %s\nMerchant account: %s\n" % (settings.CURRENCY_ABBREV, notification.amount, method.internaldescription, notification.merchantReference, notification.pspReference, notification.merchantAccountCode)) notification.confirmed = True notification.save() # For manual payments, we can only create an open-ended entry # in the accounting accstr = "Manual Adyen payment: %s (%s)" % ( notification.merchantReference, notification.pspReference) accrows = [ (pm.config('accounting_authorized'), accstr, trans.amount, None), ] create_accounting_entry(date.today(), accrows, True, urls) return # Process a payment on the primary account manager = InvoiceManager() try: # Figure out the invoiceid if not notification.merchantReference.startswith( pm.config('merchantref_prefix')): raise AdyenProcessingException( 'Merchant reference does not start with %s' % pm.config('merchantref_prefix')) invoiceid = int( notification. merchantReference[len(pm.config('merchantref_prefix')):]) # Get the actual invoice try: invoice = Invoice.objects.get(pk=invoiceid) except Invoice.DoesNotExist: raise AdyenProcessingException( 'Invoice with id %s does not exist' % invoiceid) def invoice_logger(msg): invoice_logger.invoice_log += msg invoice_logger.invoice_log += "\n" invoice_logger.invoice_log = "" # Handle our special case where an IBAN notification comes in on the creditcard # processor (the primary one), but needs to be flagged as the other one. # If it can't be found, we just flag it on the other method, since the only # thing lost is some statistics. if trans.method == 'bankTransfer_IBAN': # Find our related method, if it exists mlist = list( InvoicePaymentMethod.objects.filter( classname= 'postgresqleu.util.payment.adyen.AdyenBanktransfer'). extra( where=["config->>'merchantaccount' = %s"], params=[pm.config('merchantaccount')], )) if len(mlist) == 1: usedmethod = mlist[0] else: usedmethod = method else: usedmethod = method (status, _invoice, _processor) = manager.process_incoming_payment_for_invoice( invoice, notification.amount, 'Adyen id %s' % notification.pspReference, 0, pm.config('accounting_authorized'), 0, urls, invoice_logger, usedmethod) if status != manager.RESULT_OK: # An error occurred, but nevertheless the money is in our account at this # point. The invoice itself will not have been flagged as paid since something # went wrong, and this also means no full accounting record has been created. # At this point we have no transaction cost, so we just have the payment itself. # Someone will manually have to figure out where to stick it. accrows = [ (pm.config('accounting_authorized'), "Incorrect payment for invoice #{0}".format(invoice.id), notification.amount, None), ] create_accounting_entry(date.today(), accrows, True, urls) send_simple_mail( settings.INVOICE_SENDER_EMAIL, pm.config('notification_receiver'), 'Error processing invoice from Adyen notification', "An error occured processing the notification for invoice #{0} using {1}.\n\nThe messages given were:\n{2}\n\nAn incomplete accounting record has been created, and the situation needs to be handled manually.\n" .format(invoice.id, method.internaldescription, invoice_logger.invoice_log), ) # Actually flag the notification as handled, so we don't end up repeating it. notification.confirmed = True notification.save() return if invoice.accounting_object: # Store the accounting object so we can properly tag the # fee for it when we process the settlement (since we don't # actually know the fee yet) trans.accounting_object = invoice.accounting_object trans.save() # If nothing went wrong, then this invoice is now fully # flagged as paid in the system. send_simple_mail( settings.INVOICE_SENDER_EMAIL, pm.config('notification_receiver'), 'Adyen payment authorized', "An Adyen payment of %s%s with reference %s was authorized on the Adyen platform for %s.\nInvoice: %s\nRecipient name: %s\nRecipient user: %s\nPayment method: %s\nAdyen reference: %s\n" % (settings.CURRENCY_ABBREV, notification.amount, notification.merchantReference, method.internaldescription, invoice.title, invoice.recipient_name, invoice.recipient_email, notification.paymentMethod, notification.pspReference)) except AdyenProcessingException as ex: # Generate an email telling us about this exception! send_simple_mail( settings.INVOICE_SENDER_EMAIL, pm.config('notification_receiver'), 'Exception occured processing Adyen notification', "An exception occurred processing the notification for %s on %s:\n\n%s\n" % (notification.merchantReference, method.internaldescription, ex)) # We have stored the notification already, but we want # to make sure it's not *confirmed*. That way it'll keep # bugging the user. So, return here instead of confirming # it. return else: send_simple_mail( settings.INVOICE_SENDER_EMAIL, pm.config('notification_receiver'), 'Unsuccessful Adyen authorization received', "A credit card authorization for %s on account %s has failed.\nThe reason given was:\n%s\n\nYou don't need to take any further action, nothing has been confirmed in the systems." % ( notification.merchantReference, notification.merchantAccountCode, notification.reason, )) notification.confirmed = True notification.save()
def adyen_return_handler(request, methodid): method = get_object_or_404(InvoicePaymentMethod, pk=methodid, active=True) pm = method.get_implementation() sig = pm.calculate_signature(request.GET) if sig != request.GET['merchantSig']: return render(request, 'adyen/sigerror.html') # We're going to need the invoice for pretty much everything, # so attempt to find it. if request.GET['merchantReturnData'] != request.GET[ 'merchantReference'] or not request.GET[ 'merchantReturnData'].startswith( pm.config('merchantref_prefix')): AdyenLog(pspReference='', message='Return handler received invalid reference %s/%s' % (request.GET['merchantReturnData'], request.GET['merchantReference']), error=True, paymentmethod=method).save() return render( request, 'adyen/invalidreference.html', { 'reference': "%s//%s" % (request.GET['merchantReturnData'], request.GET['merchantReference']), }) invoiceid = int(request.GET['merchantReturnData'] [len(pm.config('merchantref_prefix')):]) try: invoice = Invoice.objects.get(pk=invoiceid) except Invoice.DoesNotExist: AdyenLog( pspReference='', message='Return handler could not find invoice for reference %s' % request.GET['merchantReturnData'], error=True, paymentmethod=method).save() return render(request, 'adyen/invalidreference.html', { 'reference': request.GET['merchantReturnData'], }) manager = InvoiceManager() if invoice.processor: processor = manager.get_invoice_processor(invoice) returnurl = processor.get_return_url(invoice) else: if invoice.recipient_user: returnurl = "%s/invoices/%s/" % (settings.SITEBASE, invoice.pk) else: returnurl = "%s/invoices/%s/%s/" % (settings.SITEBASE, invoice.pk, invoice.recipient_secret) AdyenLog(pspReference='', message='Return handler received %s result for %s' % (request.GET['authResult'], request.GET['merchantReturnData']), error=False, paymentmethod=method).save() if request.GET['authResult'] == 'REFUSED': return render(request, 'adyen/refused.html', { 'url': returnurl, }) elif request.GET['authResult'] == 'CANCELLED': return HttpResponseRedirect(returnurl) elif request.GET['authResult'] == 'ERROR': return render(request, 'adyen/transerror.html', { 'url': returnurl, }) elif request.GET['authResult'] == 'PENDING': return render(request, 'adyen/pending.html', { 'url': returnurl, }) elif request.GET['authResult'] == 'AUTHORISED': # NOTE! Adyen strongly recommends not reacting on # authorized values, but deal with them from the # notifications instead. So we'll do that. # However, if we reach this point and it's actually # already dealt with by the notification arriving # asynchronously, redirect the user properly. if invoice.paidat: # Yup, it's paid, so send the user off to the page # that they came from. return HttpResponseRedirect(returnurl) # Show the user a pending message. The refresh time is dependent # on how many times we've seen this one before. status, created = ReturnAuthorizationStatus.objects.get_or_create( pspReference=request.GET['pspReference']) status.seencount += 1 status.save() return render(request, 'adyen/authorized.html', { 'refresh': 3**status.seencount, 'url': returnurl, }) else: return render(request, 'adyen/invalidresult.html', { 'result': request.GET['authResult'], })
def webhook(request, methodid): sig = request.META['HTTP_STRIPE_SIGNATURE'] try: payload = json.loads(request.body.decode('utf8', errors='ignore')) except ValueError: return HttpResponse("Invalid JSON", status=400) method = InvoicePaymentMethod.objects.get(pk=methodid, classname="postgresqleu.util.payment.stripe.Stripe") pm = method.get_implementation() sigdata = dict([v.strip().split('=') for v in sig.split(',')]) sigstr = sigdata['t'] + '.' + request.body.decode('utf8', errors='ignore') mac = hmac.new(pm.config('webhook_secret').encode('utf8'), msg=sigstr.encode('utf8'), digestmod=hashlib.sha256) if mac.hexdigest() != sigdata['v1']: return HttpResponse("Invalid signature", status=400) # Signature is OK, figure out what to do if payload['type'] == 'checkout.session.completed': sessionid = payload['data']['object']['id'] try: co = StripeCheckout.objects.get(sessionid=sessionid) except StripeCheckout.DoesNotExist: StripeLog(message="Received completed session event for non-existing sessions {}".format(sessionid), error=True, paymentmethod=method).save() return HttpResponse("OK") # We don't get enough data in the session, unfortunately, so we have to # make some incoming API calls. StripeLog(message="Received Stripe webhook for checkout {}. Processing.".format(co.id), paymentmethod=method).save() process_stripe_checkout(co) StripeLog(message="Completed processing webhook for checkout {}.".format(co.id), paymentmethod=method).save() return HttpResponse("OK") elif payload['type'] == 'charge.refunded': chargeid = payload['data']['object']['id'] # There can be multiple refunds on each charge, so we have to look through all the # possible ones, and compare. Unfortunately, there is no notification available which # tells us *which one* was completed. Luckily there will never be *many* refunds on a # single charge. with transaction.atomic(): for r in payload['data']['object']['refunds']['data']: try: refund = StripeRefund.objects.get(paymentmethod=method, chargeid=chargeid, refundid=r['id']) except StripeRefund.DoesNotExist: StripeLog(message="Received completed refund event for non-existant refund {}".format(r['id']), error=True, paymentmethod=method).save() return HttpResponse("OK") if refund.completedat: # It wasn't this one, as it's already been completed. continue if r['amount'] != refund.amount * 100: StripeLog(message="Received completed refund with amount {0} instead of expected {1} for refund {2}".format(r['amount'] / 100, refund.amount, refund.id), error=True, paymentmethod=method).save() return HttpResponse("OK") # OK, refund looks fine StripeLog(message="Received Stripe webhook for refund {}. Processing.".format(refund.id), paymentmethod=method).save() refund.completedat = datetime.now() refund.save() manager = InvoiceManager() manager.complete_refund( refund.invoicerefundid_id, refund.amount, 0, # Unknown fee pm.config('accounting_income'), pm.config('accounting_fee'), [], method) return HttpResponse("OK") elif payload['type'] == 'payout.paid': # Payout has left Stripe. Should include both automatic and manual ones payoutid = payload['data']['object']['id'] obj = payload['data']['object'] if obj['currency'].lower() != settings.CURRENCY_ISO.lower(): StripeLog(message="Received payout in incorrect currency {}, ignoring".format(obj['currency']), error=True, paymentmethod=method).save() return HttpResponse("OK") with transaction.atomic(): if StripePayout.objects.filter(payoutid=payoutid).exists(): StripeLog(message="Received duplicate notification for payout {}, ignoring".format(payoutid), error=True, paymentmethod=method).save() return HttpResponse("OK") payout = StripePayout(paymentmethod=method, payoutid=payoutid, amount=Decimal(obj['amount']) / 100, sentat=datetime.now(), description=obj['description']) payout.save() acctrows = [ (pm.config('accounting_income'), 'Stripe payout {}'.format(payout.payoutid), -payout.amount, None), (pm.config('accounting_payout'), 'Stripe payout {}'.format(payout.payoutid), payout.amount, None), ] if is_managed_bank_account(pm.config('accounting_payout')): entry = create_accounting_entry(date.today(), acctrows, True) # XXX: we don't know what this looks like at the other end yet, so put a random string in there register_pending_bank_matcher(pm.config('accounting_payout'), '.*STRIPE_PAYOUT_WHAT_GOES_HERE?.*', payout.amount, entry) msg = "A Stripe payout with description {} completed for {}.\n\nAccounting entry {} was created and will automatically be closed once the payout has arrived.".format( payout.description, method.internaldescription, entry, ) else: msg = "A Stripe payout with description {} completed for {}.\n".format(payout.description, method.internaldescription) StripeLog(message=msg, paymentmethod=method).save() send_simple_mail(settings.INVOICE_SENDER_EMAIL, pm.config('notification_receiver'), 'Stripe payout completed', msg, ) return HttpResponse("OK") else: StripeLog(message="Received unknown Stripe event type '{}'".format(payload['type']), error=True, paymentmethod=method).save() # We still flag it as OK to stripe return HttpResponse("OK")
def create_sponsor_invoice(user, sponsor): conference = sponsor.conference level = sponsor.level if settings.EU_VAT: # If a sponsor has an EU VAT Number, we do *not* charge VAT. # For any sponsor without a VAT number, charge VAT. # Except if the sponsor is from outside the EU, in which case no VAT. # If a sponsor is from our home country, meaning they have a # VAT number and it starts with our prefix, charge VAT. # XXX: we should probably have *accounting* entries for reverse # VAT on the ones with a number, but EU vat is currently # handled manually outside the process for now. if sponsor.vatstatus == 0: # Sponsor inside EU with VAT number if not sponsor.vatnumber: raise Exception("Cannot happen") if sponsor.vatnumber.startswith(settings.EU_VAT_HOME_COUNTRY): # Home country, so we charge vat vatlevel = conference.vat_sponsorship reverse_vat = False else: # Not home country but has VAT number vatlevel = None reverse_vat = True elif sponsor.vatstatus == 1: # Sponsor inside EU but no VAT number vatlevel = conference.vat_sponsorship reverse_vat = False else: # Sponsor outside EU vatlevel = None reverse_vat = False else: # Not caring about EU VAT, so assign whatever the conference said vatlevel = conference.vat_sponsorship reverse_vat = False invoicerows = [ ['%s %s sponsorship' % (conference, level), 1, level.levelcost, vatlevel], ] if conference.startdate < date.today() + timedelta(days=5): # If conference happens in the next 5 days, invoice is due immediately duedate = date.today() elif conference.startdate < date.today() + timedelta(days=30): # Less than 30 days before the conference, set the due date to # 5 days before the conference duedate = conference.startdate - timedelta(days=5) else: # More than 30 days before the conference, set the due date # to 30 days from now. duedate = datetime.now() + timedelta(days=30) manager = InvoiceManager() processor = invoicemodels.InvoiceProcessor.objects.get(processorname="confsponsor processor") i = manager.create_invoice( user, user.email, user.first_name + ' ' + user.last_name, get_sponsor_invoice_address(sponsor.name, sponsor.invoiceaddr, sponsor.vatnumber), '%s sponsorship' % conference.conferencename, datetime.now(), duedate, invoicerows, processor=processor, processorid=sponsor.pk, accounting_account=settings.ACCOUNTING_CONFSPONSOR_ACCOUNT, accounting_object=conference.accounting_object, reverse_vat=reverse_vat, extra_bcc_list=conference.sponsoraddr, paymentmethods=level.paymentmethods.all(), ) return i