Exemple #1
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")
Exemple #2
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)
Exemple #3
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()
Exemple #4
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
    def process_one_account(self, method):
        pm = method.get_implementation()

        trustly = Trustly(pm)
        manager = InvoiceManager()

        refunds = InvoiceRefund.objects.filter(completed__isnull=True,
                                               invoice__paidusing=method)

        for r in refunds:
            # Find the matching Trustly transaction
            trustlytransactionlist = list(
                TrustlyTransaction.objects.filter(invoiceid=r.invoice.pk,
                                                  paymentmethod=method))
            if len(trustlytransactionlist) == 0:
                raise CommandError(
                    "Could not find trustly transaction for invoice {0}".
                    format(r.invoice.pk))
            elif len(trustlytransactionlist) != 1:
                raise CommandError(
                    "Found {0} trustly transactions for invoice {1}!".format(
                        len(trustlytransactionlist), r.invoice.pk))
            trustlytrans = trustlytransactionlist[0]
            w = trustly.getwithdrawal(trustlytrans.orderid)
            if not w:
                # No refund yet
                continue

            if w['transferstate'] != 'CONFIRMED':
                # Still pending
                continue

            if w['currency'] != settings.CURRENCY_ABBREV:
                # If somebody paid in a different currency (and Trustly converted it for us),
                # the withdrawal entry is specified in the original currency, which is more than
                # a little annoying. To deal with it, attempt to fetch the ledger for the day
                # and if we can find it there, use the amount from that one.
                day = dateutil.parser.parse(w['datestamp']).date()
                ledgerrows = trustly.getledgerforday(day)
                for lr in ledgerrows:
                    if int(lr['orderid']) == trustlytrans.orderid and lr[
                            'accountname'] == 'BANK_WITHDRAWAL_QUEUED':
                        # We found the corresponding accounting row. So we take the amount from
                        # this and convert the difference to what we expeced into the fee. This
                        # can end up being a negative fee, but it should be small enough that
                        # it's not a real problem.
                        fees = (
                            r.fullamount +
                            Decimal(lr['amount']).quantize(Decimal('0.01')))
                        TrustlyLog(
                            message=
                            "Refund for order {0}, invoice {1}, was made as {2} {3} instead of {4} {5}. Using ledger mapped to {6} {7} with difference of {8} {9} booked as fees"
                            .format(
                                trustlytrans.orderid,
                                r.invoice.pk,
                                Decimal(w['amount']),
                                w['currency'],
                                r.fullamount,
                                settings.CURRENCY_ABBREV,
                                Decimal(lr['amount']).quantize(
                                    Decimal('0.01')),
                                settings.CURRENCY_ABBREV,
                                fees,
                                settings.CURRENCY_ABBREV,
                            ),
                            error=False,
                            paymentmethod=method,
                        ).save()
                        break
                else:
                    # Unable to find the refund in the ledger. This could be a matter of timing,
                    # so yell about it but try agian.
                    raise CommandError(
                        "Trustly refund for invoice {0} was made in {1} instead of {2}, but could not be found in ledger."
                        .format(r.invoice.pk, w['currency'],
                                settings.CURRENCY_ABBREV))
            else:
                # Currency is correct, so check that the refunded amount is the same as
                # the one we expected.
                if Decimal(w['amount']) != r.fullamount:
                    raise CommandError(
                        "Mismatch in amount on Trustly refund for invoice {0} ({1} vs {2})"
                        .format(r.invoice.pk, Decimal(w['amount']),
                                r.fullamount))
                fees = 0

            # Ok, things look good!
            TrustlyLog(
                message="Refund for order {0}, invoice {1}, completed".format(
                    trustlytrans.orderid, r.invoice.pk),
                error=False,
                paymentmethod=method).save()
            manager.complete_refund(r.id, r.fullamount, fees,
                                    pm.config('accounting_income'),
                                    pm.config('accounting_fee'), [], method)