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)
Beispiel #2
0
    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()
Beispiel #3
0
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)
Beispiel #5
0
    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)
Beispiel #6
0
    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)
Beispiel #7
0
    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)
Beispiel #8
0
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'])
Beispiel #9
0
    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!"
Beispiel #11
0
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',
        })
Beispiel #12
0
    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
Beispiel #13
0
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()
Beispiel #14
0
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()
Beispiel #15
0
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
Beispiel #16
0
    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
Beispiel #17
0
		# 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!
Beispiel #18
0
    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()
Beispiel #19
0
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")
Beispiel #20
0
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