def process_payment_accounting_report(self, report): method = report.paymentmethod pm = method.get_implementation() sio = io.StringIO(report.contents) reader = csv.DictReader(sio, delimiter=',') for l in reader: # SentForSettle is what we call capture, so we track that # Settled is when we actually receive the money # Changes in Sep 2015 means Settled is sometimes SettledBulk # Everything else we ignore if l['Record Type'] == 'SentForSettle' or l['Record Type'] == 'Settled' or l['Record Type'] == 'SettledBulk': # Find the actual payment pspref = l['Psp Reference'] bookdate = l['Booking Date'] try: trans = TransactionStatus.objects.get(pspReference=pspref, paymentmethod=method) except TransactionStatus.DoesNotExist: # Yes, for now we rollback the whole processing of this one raise Exception('Transaction %s not found!' % pspref) if l['Record Type'] == 'SentForSettle': # If this is a POS transaction, it typically received a # separate CAPTURE notification, in which case the capture # date is already set. But if not, we'll set it to the # sent for settle date. if not trans.capturedat: trans.capturedat = bookdate trans.method = l['Payment Method'] trans.save() AdyenLog(message='Transaction %s captured at %s' % (pspref, bookdate), error=False, paymentmethod=method).save() if self.verbose: self.stdout.write("Sent for settle on {0}".format(pspref)) elif l['Record Type'] in ('Settled', 'SettledBulk'): if trans.settledat is not None: # Transaction already settled. But we might be reprocessing # the report, so verify if the previously settled one is # *identical*. if trans.settledamount == Decimal(l['Main Amount']).quantize(Decimal('0.01')): self.stderr.write("Transaction {0} already settled at {2}, ignoring (NOT creating accounting record)!".format(pspref, trans.settledat)) continue else: raise CommandError('Transaction {0} settled more than once?!'.format(pspref)) if not trans.capturedat: trans.capturedat = bookdate trans.settledat = bookdate trans.settledamount = Decimal(l['Main Amount']).quantize(Decimal('0.01')) trans.save() if self.verbose: self.stdout.write("Settled {0}, total amount {1}".format(pspref, trans.settledamount)) AdyenLog(message='Transaction %s settled at %s' % (pspref, bookdate), error=False, paymentmethod=method).save() # Settled transactions create a booking entry accstr = "Adyen settlement %s" % pspref accrows = [ (pm.config('accounting_authorized'), accstr, -trans.amount, None), (pm.config('accounting_payable'), accstr, trans.settledamount, None), (pm.config('accounting_fee'), accstr, trans.amount - trans.settledamount, trans.accounting_object), ] create_accounting_entry(accrows, False)
def complete_refund(self, refundid, refundamount, refundfee, incomeaccount, costaccount, extraurls, method): # Process notification from payment provider that refund has completed refund = InvoiceRefund.objects.get(id=refundid) invoice = refund.invoice if refund.completed: raise Exception( "Refund {0} has already been completed".format(refundid)) if not refund.issued: raise Exception( "Refund {0} has not been issued, yet signaled completed!". format(refundid)) if refundamount != refund.amount + refund.vatamount: raise Exception( "Refund {0} attempted to process amount {1} but refund should be {2}" .format(refundid, refundamount, refund.amount + refund.vatamount)) accountingtxt = 'Refund ({0}) of invoice #{1}'.format( refundid, invoice.id) accrows = [ (incomeaccount, accountingtxt, -(refundamount - refundfee), None), ] if refund.vatamount: accrows.append((refund.vatrate.vataccount.num, accountingtxt, refund.vatamount, None), ) if refundfee != 0: accrows.append((costaccount, accountingtxt, -refundfee, invoice.accounting_object), ) if invoice.accounting_account: accrows.append( (invoice.accounting_account, accountingtxt, refundamount - refund.vatamount, invoice.accounting_object), ) leaveopen = False else: leaveopen = True urls = [ '%s/invoices/%s/' % (settings.SITEBASE, invoice.pk), ] if extraurls: urls.extend(extraurls) create_accounting_entry(date.today(), accrows, leaveopen, urls) # Also flag the refund as done refund.completed = datetime.now() refund.save() wrapper = InvoiceWrapper(invoice) wrapper.email_refund_sent(refund) InvoiceHistory(invoice=invoice, txt='Completed refund {0}'.format(refund.id)).save()
def process_refund(notification): # Store the refund, and send an email! if notification.success: try: ts = TransactionStatus.objects.get(pspReference=notification.originalReference) refund = Refund(notification=notification, transaction=ts, refund_amount=notification.amount) refund.save() # 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. urls = [ "https://ca-live.adyen.com/ca/ca/accounts/showTx.shtml?pspReference=%s&txType=Payment&accountKey=MerchantAccount.%s" % (notification.pspReference, notification.merchantAccountCode), ] accrows = [ (settings.ACCOUNTING_ADYEN_REFUNDS_ACCOUNT, "Refund of %s (transaction %s) " % (ts.notes, ts.pspReference), -refund.refund_amount, None), ] send_simple_mail(settings.INVOICE_SENDER_EMAIL, settings.ADYEN_NOTIFICATION_RECEIVER, 'Adyen refund received', "A refund of %s%s for transaction %s was processed\n\nNOTE! You must complete the accounting system entry manually for refunds!" % (settings.CURRENCY_ABBREV, notification.amount, notification.originalReference)) create_accounting_entry(date.today(), accrows, True, urls) except TransactionStatus.DoesNotExist: send_simple_mail(settings.INVOICE_SENDER_EMAIL, settings.ADYEN_NOTIFICATION_RECEIVER, 'Adyen refund received for nonexisting transaction', "A refund for %s was received, but the transaction does not exist!\n\nYou probably want to investigate this!\n" % notification.originalReference) else: send_simple_mail(settings.INVOICE_SENDER_EMAIL, settings.ADYEN_NOTIFICATION_RECEIVER, 'Unsuccessful adyen refund received', "A refund for %s has failed.\nThe reason given was:\n%s\n\nYou probably want to investigate this!\n" % ( notification.merchantReference, notification.reason)) notification.confirmed = True notification.save()
def fetch_one_account(self, method): pm = method.get_implementation() trustly = Trustly(pm) transactions = trustly.getledgerforrange( datetime.today() - timedelta(days=7), datetime.today()) for t in transactions: if t['accountname'] == 'BANK_WITHDRAWAL_QUEUED' and not t[ 'orderid']: # If it has an orderid, it's a refund, but if not, then it's a transfer out (probably) w, created = TrustlyWithdrawal.objects.get_or_create( paymentmethod=method, gluepayid=t['gluepayid'], defaults={ 'amount': -Decimal(t['amount']), 'message': t['messageid'], }, ) w.save() if created: TrustlyLog( message='New bank withdrawal of {0} found'.format( -Decimal(t['amount'])), paymentmethod=method).save() accstr = 'Transfer from Trustly to bank' accrows = [ (pm.config('accounting_income'), accstr, -w.amount, None), (pm.config('accounting_transfer'), accstr, w.amount, None), ] entry = create_accounting_entry( dateutil.parser.parse(t['datestamp']).date(), accrows, True, [], ) if is_managed_bank_account( pm.config('accounting_transfer')): register_pending_bank_matcher( pm.config('accounting_transfer'), '.*TRUSTLY.*{0}.*'.format(w.gluepayid), w.amount, entry)
def process_incoming_payment_for_invoice(self, invoice, transamount, transdetails, transcost, incomeaccount, costaccount, extraurls, logger, method): # Do the same as process_incoming_payment, but assume that the # invoice has already been matched by other means. invoiceid = invoice.pk if not invoice.finalized: logger("Invoice %s was never sent!" % invoiceid) return (self.RESULT_NOTSENT, None, None) if invoice.ispaid: logger("Invoice %s already paid!" % invoiceid) return (self.RESULT_ALREADYPAID, None, None) if invoice.deleted: logger("Invoice %s has been deleted!" % invoiceid) return (self.RESULT_DELETED, None, None) if invoice.total_amount != transamount: logger("Invoice %s, received payment of %s, expected %s!" % (invoiceid, transamount, invoice.total_amount)) return (self.RESULT_INVALIDAMOUNT, None, None) # Things look good, flag this invoice as paid invoice.paidat = timezone.now() invoice.paymentdetails = transdetails[:100] invoice.paidusing = method # If there is a processor module registered for this invoice, # we need to instantiate it and call it. So, well, let's do # that. processor = None if invoice.processor: processor = self.get_invoice_processor(invoice, logger=logger) if not processor: # get_invoice_processor() has already logged return (self.RESULT_PROCESSORFAIL, None, None) try: with transaction.atomic(): processor.process_invoice_payment(invoice) except Exception as ex: logger("Failed to run invoice processor '%s': %s" % (invoice.processor, ex)) return (self.RESULT_PROCESSORFAIL, None, None) # Generate a PDF receipt for this, since it's now paid wrapper = InvoiceWrapper(invoice) invoice.pdf_receipt = base64.b64encode(wrapper.render_pdf_receipt()).decode('ascii') # Save and we're done! invoice.save() # Create an accounting entry for this invoice. If we have the required # information on the invoice, we can finalize it. If not, we will # need to create an open ended one. accountingtxt = 'Invoice #%s: %s' % (invoice.id, invoice.title) accrows = [ (incomeaccount, accountingtxt, invoice.total_amount - transcost, None), ] if transcost > 0: # If there was a transaction cost known at this point (which # it typically is with Paypal), make sure we book a row for it. accrows.append( (costaccount, accountingtxt, transcost, invoice.accounting_object), ) if invoice.total_vat: # If there was VAT on this invoice, create a separate accounting row for this # part. As there can in theory (though maybe not in practice?) be multiple different # VATs on the invoice, we need to summarize the rows. vatsum = defaultdict(int) for r in invoice.invoicerow_set.all(): if r.vatrate_id: vatsum[r.vatrate.vataccount.num] += (r.rowamount * r.rowcount * r.vatrate.vatpercent / Decimal(100)).quantize(Decimal('0.01')) total_vatsum = sum(vatsum.values()) if invoice.total_vat != total_vatsum: raise Exception("Stored VAT total %s does not match calculated %s" % (invoice.total_vat, total_vatsum)) for accountnum, s in list(vatsum.items()): accrows.append( (accountnum, accountingtxt, -s, None), ) if invoice.accounting_account: accrows.append( (invoice.accounting_account, accountingtxt, -(invoice.total_amount - invoice.total_vat), invoice.accounting_object), ) leaveopen = False else: leaveopen = True urls = ['%s/invoices/%s/' % (settings.SITEBASE, invoice.pk), ] if extraurls: urls.extend(extraurls) create_accounting_entry(accrows, leaveopen, urls) # Send the receipt to the user if possible - that should make # them happy :) wrapper.email_receipt() # Write a log, because it's always nice.. InvoiceHistory(invoice=invoice, txt='Processed payment').save() InvoiceLog( message="Processed payment of %s %s for invoice %s (%s)" % ( invoice.total_amount, settings.CURRENCY_ABBREV, invoice.pk, invoice.title), timestamp=timezone.now() ).save() return (self.RESULT_OK, invoice, processor)
def process_settlement_detail_report_batch(self, report): # Summarize the settlement detail report in an email to to treasurer@, so they # can keep track of what's going on. method = report.paymentmethod pm = method.get_implementation() # Get the batch number from the url batchnum = re.search('settlement_detail_report_batch_(\d+).csv$', report.url).groups(1)[0] # Now summarize the contents sio = io.StringIO(report.contents) reader = csv.DictReader(sio, delimiter=',') types = {} for l in reader: t = l['Type'] if t == 'Balancetransfer': # Balance transfer is special -- we can have two of them that evens out, # but we need to separate in and out if Decimal(l['Net Debit (NC)'] or 0) > 0: t = "Balancetransfer2" lamount = Decimal(l['Net Credit (NC)'] or 0) - Decimal( l['Net Debit (NC)'] or 0) if t in types: types[t] += lamount else: types[t] = lamount def sort_types(a): # Special sort method that just ensures that Settled always ends up at the top # and the rest is just alphabetically sorted. (And yes, this is ugly code :P) if a[0] == 'Settled' or a[0] == 'SettledBulk': return 'AAA' return a[0] msg = "\n".join([ "%-20s: %s" % (k, v) for k, v in sorted(iter(types.items()), key=sort_types) ]) acct = report.notification.merchantAccountCode # Generate an accounting record, iff we know what every row on the # statement actually is. acctrows = [] accstr = "Adyen settlement batch %s for %s" % (batchnum, acct) payout_amount = 0 for t, amount in list(types.items()): if t == 'Settled' or t == 'SettledBulk': # Settled means we took it out of the payable balance acctrows.append( (pm.config('accounting_payable'), accstr, -amount, None)) elif t == 'MerchantPayout': # Amount directly into our checking account acctrows.append( (pm.config('accounting_payout'), accstr, -amount, None)) # Payouts show up as negative, so we have to reverse the sign payout_amount -= amount elif t in ('DepositCorrection', 'Balancetransfer', 'Balancetransfer2', 'ReserveAdjustment'): # Modification of our deposit account - in either direction! acctrows.append( (pm.config('accounting_merchant'), accstr, -amount, None)) elif t == 'InvoiceDeduction': # Adjustment of the invoiced costs. So adjust the payment fees! acctrows.append( (pm.config('accounting_fee'), accstr, -amount, None)) elif t == 'Refunded' or t == 'RefundedBulk': # Refunded - should already be booked against the refunding account acctrows.append( (pm.config('accounting_refunds'), accstr, -amount, None)) else: # Other rows that we don't know about will generate an open accounting entry # for manual fixing. pass if len(acctrows) == len(types): # If all entries were processed, the accounting entry should # automatically be balanced by now, so we can safely just complete it. # If payout is to a managed bank account (and there is a payout), we register # the payout for processing there and leave the entry open. If not, then we # just close it right away. is_managed = is_managed_bank_account( pm.config('accounting_payout')) if is_managed and payout_amount > 0: entry = create_accounting_entry(date.today(), acctrows, True) # Register a pending bank transfer using the syntax that Adyen are # currently using. We only match the most important keywords, just # but it should still be safe against most other possibilities. register_pending_bank_matcher( pm.config('accounting_payout'), '.*ADYEN.*BATCH {0} .*'.format(batchnum), payout_amount, entry) msg = "A settlement batch with Adyen has completed for merchant account %s. A summary of the entries are:\n\n%s\n\nAccounting entry %s was created and will automatically be closed once the payout has arrived." % ( acct, msg, entry) else: # Close immediately create_accounting_entry(date.today(), acctrows, False) msg = "A settlement batch with Adyen has completed for merchant account %s. A summary of the entries are:\n\n%s\n\n" % ( acct, msg) else: # All entries were not processed, so we write what we know to the # db, and then just leave the entry open. create_accounting_entry(date.today(), acctrows, True) msg = "A settlement batch with Adyen has completed for merchant account %s. At least one entry in this was UNKNOWN, and therefor the accounting record has been left open, and needs to be adjusted manually!\nA summary of the entries are:\n\n%s\n\n" % ( acct, msg) send_simple_mail(settings.INVOICE_SENDER_EMAIL, pm.config('notification_receiver'), 'Adyen settlement batch %s completed' % batchnum, msg)
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 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
allentries.append({ 'date': atrans.settledat.date(), 'text': 'Adyen settlement %s' % atrans.pspReference, 'rows': [ (settings.ACCOUNTING_ADYEN_AUTHORIZED_ACCOUNT, -atrans.amount, None), (settings.ACCOUNTING_ADYEN_PAYABLE_ACCOUNT, atrans.settledamount, None), (settings.ACCOUNTING_ADYEN_FEE_ACCOUNT, atrans.amount-atrans.settledamount, None), ], 'leaveopen': False, }) allentries.sort(key=lambda e: e['date']) # Now is when we create the actual records... for entry in allentries: try: create_accounting_entry( entry['date'], [(r[0], entry['text'], r[1], r[2]) for r in entry['rows']], entry['leaveopen']) except: print "Failed on this entry:" pprint(entry) raise print "Created %s entries" % len(allentries) while True: if raw_input("Does this seem reasonable? Type 'yes' to commit, or hit ctrl-c to abort. So? ") == 'yes': break print "All done!"
def banktransactions(request): authenticate_backend_group(request, 'Invoice managers') if request.method == 'POST': if 'submit' not in request.POST: return HttpResponseRedirect(".") if 'transid' in request.POST: trans = get_object_or_404(PendingBankTransaction, id=get_int_or_error( request.POST, 'transid')) if request.POST['submit'] == 'Discard': InvoiceLog( message="Discarded bank transaction of {0}{1} with text {2}" .format(trans.amount, settings.CURRENCY_ABBREV, trans.transtext)).save() trans.delete() messages.info(request, "Transaction discarded") return HttpResponseRedirect(".") elif request.POST['submit'] == 'Create accounting record': pm = trans.method.get_implementation() accrows = [ (pm.config('bankaccount'), trans.transtext, trans.amount, None), ] entry = create_accounting_entry(date.today(), accrows, True) InvoiceLog( message= "Created manual accounting entry for transaction of {0}{1} with text {2}" .format(trans.amount, settings.CURRENCY_ABBREV, trans.transtext)).save() trans.delete() return HttpResponseRedirect("/accounting/e/{0}/".format( entry.id)) elif request.POST['submit'] == 'Return to sender': pm = trans.method.get_implementation() pm.return_payment(trans) InvoiceLog( message= "Scheduled transaction '{0}' ({1}{2}) for return to sender using {3}" .format(trans.transtext, trans.amount, settings.CURRENCY_ABBREV, trans.method.internaldescription)).save() trans.delete() return HttpResponseRedirect(".") else: raise Http404("Invalid request") elif 'matcherid' in request.POST: matcher = get_object_or_404(PendingBankMatcher, pk=get_int_or_error( request.POST, 'matcherid')) if request.POST['submit'] == 'Discard': InvoiceLog( message="Discarded pending bank matcher {0} for {1} {2}". format(matcher.pattern, matcher.amount, settings.CURRENCY_ABBREV)).save() matcher.delete() messages.info(request, "Matcher discarded") return HttpResponseRedirect(".") else: raise Http404("Invalid request") else: raise Http404("Invalid request") pendingtransactions = PendingBankTransaction.objects.order_by('created') pendingmatchers = PendingBankMatcher.objects.order_by('created') return render( request, 'invoices/banktransactions.html', { 'transactions': pendingtransactions, 'matchers': pendingmatchers, 'topadmin': 'Invoices', 'helplink': 'payment', })
def refund_invoice(self, invoice, reason, amount, vatamount, vatrate): # Initiate a refund of an invoice if there is a payment provider that supports it. # Otherwise, flag the invoice as refunded, and assume the user took care of it manually. r = InvoiceRefund(invoice=invoice, reason=reason, amount=amount, vatamount=vatamount, vatrate=vatrate) r.save() InvoiceHistory(invoice=invoice, txt='Registered refund of {0}{1}'.format( settings.CURRENCY_SYMBOL, amount + vatamount)).save() wrapper = InvoiceWrapper(invoice) if invoice.can_autorefund: # Send an initial notice to the user. wrapper.email_refund_initiated(r) # Accounting record is created when we send the API call to the # provider. InvoiceLog( timestamp=datetime.now(), message="Initiated refund of {0}{1} of invoice {2}: {3}". format(settings.CURRENCY_SYMBOL, amount + vatamount, invoice.id, reason), ).save() else: # No automatic refund, so this is flagging something that has # already been done. Update accordingly. r.issued = r.registered r.completed = r.registered r.payment_reference = "MANUAL" r.save() # Create accounting record, since we flagged it manually. As we # don't know which account it was refunded from, leave that # end open. if invoice.accounting_account: accountingtxt = 'Refund of invoice #{0}: {1}'.format( invoice.id, invoice.title) accrows = [ (invoice.accounting_account, accountingtxt, invoice.total_amount - vatamount, invoice.accounting_object), ] if vatamount: accrows.append((r.vatrate.vataccount.num, accountingtxt, vatamount, None), ) urls = [ '%s/invoices/%s/' % (settings.SITEBASE, invoice.pk), ] create_accounting_entry(date.today(), accrows, True, urls) InvoiceHistory(invoice=invoice, txt='Flagged refund of {0}{1}'.format( settings.CURRENCY_SYMBOL, amount + vatamount)).save() wrapper.email_refund_sent(r) InvoiceLog( timestamp=datetime.now(), message="Flagged invoice {0} as refunded by {1}{2}: {3}". format(invoice.id, settings.CURRENCY_SYMBOL, amount + vatamount, reason), ).save() return r
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 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()
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 refund_invoice(self, invoice, reason, amount, vatamount, vatrate): # Initiate a refund of an invoice if there is a payment provider that supports it. # Otherwise, flag the invoice as refunded, and assume the user took care of it manually. # Validate that we're not refunding more than there should be already = invoice.invoicerefund_set.all().aggregate(amount=Coalesce(Sum('amount'), 0), vatamount=Coalesce(Sum('vatamount'), 0)) if vatamount > invoice.total_vat - already['vatamount']: raise Exception("Trying to refund more VAT than what remains on invoice!") if amount > invoice.total_amount - invoice.total_vat - already['amount']: raise Exception("Trying to refund more non-VAT than what remains on invoice!") r = InvoiceRefund(invoice=invoice, reason=reason, amount=amount, vatamount=vatamount, vatrate=vatrate) r.save() InvoiceHistory(invoice=invoice, txt='Registered refund of {0}{1}'.format(settings.CURRENCY_SYMBOL, amount + vatamount)).save() wrapper = InvoiceWrapper(invoice) if invoice.can_autorefund: # Send an initial notice to the user. wrapper.email_refund_initiated(r) # Accounting record is created when we send the API call to the # provider. InvoiceLog(timestamp=timezone.now(), message="Initiated refund of {0}{1} of invoice {2}: {3}".format(settings.CURRENCY_SYMBOL, amount + vatamount, invoice.id, reason), ).save() else: # No automatic refund, so this is flagging something that has # already been done. Update accordingly. r.issued = r.registered r.completed = r.registered r.payment_reference = "MANUAL" r.save() # Create accounting record, since we flagged it manually. As we # don't know which account it was refunded from, leave that # end open. if invoice.accounting_account: accountingtxt = 'Refund of invoice #{0}: {1}'.format(invoice.id, invoice.title) accrows = [ (invoice.accounting_account, accountingtxt, amount, invoice.accounting_object), ] if vatamount: accrows.append( (r.vatrate.vataccount.num, accountingtxt, vatamount, None) ) if 'bankaccount' in invoice.paidusing.config: accrows.append( (invoice.paidusing.config['bankaccount'], accountingtxt, -(amount + vatamount), None) ) urls = ['%s/invoices/%s/' % (settings.SITEBASE, invoice.pk), ] entry = create_accounting_entry(accrows, True, urls) if 'bankaccount' in invoice.paidusing.config: # See is_managed_bank_account(), if 'bankaccount' is present then this is # a managed bank account, and we can create a pending matcher. register_pending_bank_matcher(invoice.paidusing.config['bankaccount'], '.*Refund.*{}.+{}.*'.format(r.id, invoice.id), -(amount + vatamount), entry) InvoiceHistory(invoice=invoice, txt='Flagged refund of {0}{1}'.format(settings.CURRENCY_SYMBOL, amount + vatamount)).save() wrapper.email_refund_sent(r) InvoiceLog(timestamp=timezone.now(), message="Flagged invoice {0} as refunded by {1}{2}: {3}".format(invoice.id, settings.CURRENCY_SYMBOL, amount + vatamount, reason), ).save() send_simple_mail(settings.INVOICE_SENDER_EMAIL, settings.INVOICE_NOTIFICATION_RECEIVER, "Manual invoice flagged as refunded", """Invoice {} has been flagged as (possibly partially) refunded. This invoice does not have an automatic refund processor attached to it, which means it has to be *manually* refunded. Make sure the transfer of the refund is of {}{} and has the text Refund {} of invoice {} as payment reference if possible (to facilitate automatic matching if available). """.format(invoice.id, settings.CURRENCY_SYMBOL, amount + vatamount, r.id, invoice.id), ) return r
# Separate out donations made through our website if ti.transtext == "PostgreSQL Europe donation": 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 = [ (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!
def handle_method(self, method): pm = method.get_implementation() with transaction.atomic(): for t in BraintreeTransaction.objects.filter(Q(settledat__isnull=True) | Q(disbursedat__isnull=True), paymentmethod=method): # Process all transactions that are not settled and disbursed (ok, btrans) = pm.braintree_find(t.transid) if not ok: BraintreeLog(transid=t.transid, error=True, message='Could not find transaction {0}: {1}'.format(t.transid, btrans), paymentmethod=method).save() continue if btrans.status == 'settled': # This transaction has now been settled! Yay! # Note that this is the same status we get if it's just # settled, or also disbursed. So we need to compare that # with what's in our db. if not t.settledat: # This transaction has not been recorded as settled, but # it is now. So we mark the settlement. # Braintree don't give us the date/time for the settlement, # so just use whenever we noticed it. t.settledat = datetime.now() t.save() BraintreeLog(transid=t.transid, paymentmethod=method, message='Transaction has been settled').save() # Create an accounting row. Braintree won't tell us the # fee, and thus the actual settled amount, until after # the money has been disbursed. So assume everything # for now. accstr = "Braintree settlement {0}".format(t.transid) accrows = [ (pm.config('accounting_authorized'), accstr, -t.amount, None), (pm.config('accounting_payable'), accstr, t.amount, None), ] create_accounting_entry(date.today(), accrows, False) if t.settledat and not t.disbursedat: # Settled but not disbursed yet. But maybe it is now? if btrans.disbursement_details.success: if btrans.disbursement_details.settlement_currency_iso_code != settings.CURRENCY_ISO: BraintreeLog(transid=t.transid, error=True, paymentmethod=method, message='Transaction was disbursed in {0}, should be {1}!'.format(btrans.disbursement_details.settlement_currency_iso_code, settings.CURRENCY_ISO)).save() # No need to send an immediate email on this, we # can deal with it in the nightly batch. continue BraintreeLog(transid=t.transid, paymentmethod=method, message='Transaction has been disbursed, amount {0}, settled amount {1}'.format(btrans.amount, btrans.disbursement_details.settlement_amount)).save() t.disbursedat = btrans.disbursement_details.disbursement_date t.disbursedamount = btrans.disbursement_details.settlement_amount t.save() # Create an accounting row accstr = "Braintree disbursement {0}".format(t.transid) accrows = [ (pm.config('accounting_payable'), accstr, -t.amount, None), (pm.config('accounting_payout'), accstr, t.disbursedamount, None), ] if t.amount - t.disbursedamount > 0: accrows.append((pm.config('accounting_fee'), accstr, t.amount - t.disbursedamount, t.accounting_object)) create_accounting_entry(date.today(), accrows, False) elif datetime.today() - t.settledat > timedelta(days=10): BraintreeLog(transid=t.transid, error=True, paymentmethod=method, message='Transaction {0} was authorized on {1} and settled on {2}, but has not been disbursed yet!'.format(t.transid, t.authorizedat, t.settledat)).save() elif datetime.today() - t.authorizedat > timedelta(days=10): BraintreeLog(transid=t.transid, error=True, paymentmethod=method, message='Transaction {0} was authorized on {1}, more than 10 days ago, and has not been settled yet!'.format(t.transid, t.authorizedat)).save()
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 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