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)
Exemplo n.º 2
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")
Exemplo n.º 3
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)
Exemplo n.º 4
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)
Exemplo n.º 5
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