Ejemplo n.º 1
0
    def dispatch(self, request, *args, **kwargs):
        if not self.request.user.is_authenticated:
            return super(MySubscriptionView,
                         self).dispatch(request, *args, **kwargs)

        current_subscription = Subscription.get_current_for_user(
            self.request.user)
        waiting_subscription = Subscription.get_waiting_for_user(
            self.request.user)

        if current_subscription and current_subscription.state == Subscription.STATE_1_CANCELLED_BUT_ACTIVE and waiting_subscription:
            self.cancelled_subscription = current_subscription
            self.subscription = waiting_subscription
        elif current_subscription and current_subscription.state == Subscription.STATE_2_ACTIVE:
            self.cancelled_subscription = None
            self.subscription = current_subscription
        elif waiting_subscription:
            # theoretically, we shouldn't have a waiting subscription without a cancelled
            # on, because then the waiting one should've been activated. but maybe
            # there were payment problems on the waiting one, so better include the case
            self.cancelled_subscription = None
            self.subscription = waiting_subscription
        else:
            return redirect('wechange-payments:payment')
        return super(MySubscriptionView,
                     self).dispatch(request, *args, **kwargs)
Ejemplo n.º 2
0
def make_subscription_payment(request):
    """ A user-based view, that is used only by the plattform itself, used to make the 
        first payment for the start of a subscription. Will not allow making a payment 
        while another subscription is still active! """
    if not request.method=='POST':
        return HttpResponseNotAllowed(['POST'])
    if not request.user.is_authenticated:
        return HttpResponseForbidden('You must be logged in to do that!')
    
    update_payment = request.POST.get('update_payment', '0')
    update_payment = update_payment == '1'
    
    active_sub = Subscription.get_active_for_user(request.user)
    waiting_sub = Subscription.get_waiting_for_user(request.user)
    cancelled_sub = Subscription.get_canceled_for_user(request.user)
    
    # gatecheck: if the user has an existing active or waiting sub, and is not on the "update payment" page,
    # deny this payment (cancelled active subs are ok!)
    if not update_payment and (active_sub or waiting_sub):
        return JsonResponse({'error': _('You already have an active subscription and cannot start another one!')}, status=500)
    # gatecheck: if the user is calling an "update payment" call, and has no active or waiting subscription, deny that too
    if update_payment and not (active_sub or waiting_sub):
        return JsonResponse({'error': _('You do not currently have an active subscription you could update!')}, status=500)
        
    # if the user has an active, or a cancelled, but still active sub, or a waiting sub that has not 
    # been activated yet, we make special postponed payment that 
    # only saves the account data with the payment provider, but does not actually 
    # transfer any money yet. money will be transfered with the next subscription due date
    make_postponed = bool(active_sub or cancelled_sub)
    # if for some reason, there is ONLY a waiting sub, we will delete that later
    
    params = request.POST.copy()
    payment_type = params.get('payment_type', None)
    if payment_type in INSTANT_SUBSCRIPTION_PAYMENT_TYPES:
        # use the regular payment method and create a subscription if it was successful
        def on_success_func(payment):
            try:
                if payment.type == PAYMENT_TYPE_DIRECT_DEBIT and settings.PAYMENTS_SEPA_IS_INSTANTLY_SUCCESSFUL:
                    redirect_url = reverse('wechange_payments:payment-success', kwargs={'pk': payment.id})
                else:
                    redirect_url = reverse('wechange_payments:payment-process', kwargs={'pk': payment.id})
                data = {
                    'redirect_to': redirect_url
                }
            except:
                # exception logging happens inside create_subscription_for_payment!
                if settings.DEBUG:
                    raise
                # redirect to the success view with errors this point, so the user doesn't just resubmit the form
                redirect_url = reverse('wechange_payments:payment-success', kwargs={'pk': payment.id}) + '?subscription_error=1'
                data = {
                    'redirect_to': redirect_url
                }
            
            return JsonResponse(data)
    else:
        # use the regular payment method and redirect to a site the vendor provides
        on_success_func = None
        
    return make_payment(request, on_success_func=on_success_func, make_postponed=make_postponed)
Ejemplo n.º 3
0
def debug_delete_subscription(request):
    """ DEBUG VIEW, completely removes a subscription or processing payment. Only works during the test phase! """
    if not getattr(settings, 'PAYMENTS_TEST_PHASE', False):
        return HttpResponseForbidden('Not available.')
    if not request.user.is_authenticated:
        return HttpResponseForbidden('You must be logged in to do that!')
    subscription = Subscription.get_current_for_user(request.user)
    if subscription:
        subscription.state = Subscription.STATE_0_TERMINATED
        subscription.save()
    waiting_subscription = Subscription.get_waiting_for_user(request.user)
    if waiting_subscription:
        waiting_subscription.state = Subscription.STATE_0_TERMINATED
        waiting_subscription.save()

    processing_payment = get_object_or_None(
        Payment,
        user=request.user,
        status=Payment.STATUS_COMPLETED_BUT_UNCONFIRMED)
    if processing_payment:
        processing_payment.status = Payment.STATUS_FAILED
        processing_payment.save()
    messages.success(request,
                     'Test-server-shortcut: Your Contributions were removed!')
    return redirect('wechange-payments:overview')
Ejemplo n.º 4
0
    def dispatch(self, request, *args, **kwargs):
        if not self.request.user.is_authenticated:
            return super(PaymentView, self).dispatch(request, *args, **kwargs)

        # user needs an active or waiting subscription to access this view
        active_subscription = Subscription.get_active_for_user(
            self.request.user)
        waiting_subscription = Subscription.get_waiting_for_user(
            self.request.user)
        if not active_subscription and not waiting_subscription:
            return redirect('wechange-payments:overview')

        kwargs.update({'allow_active_subscription': True})
        return super(PaymentUpdateView, self).dispatch(request, *args,
                                                       **kwargs)
Ejemplo n.º 5
0
    def dispatch(self, request, *args, **kwargs):
        if not self.request.user.is_authenticated:
            return super(PaymentProcessView,
                         self).dispatch(request, *args, **kwargs)

        self.object = self.get_object()
        # must be owner of the payment
        if not self.object.user == self.request.user and not check_user_superuser(
                self.request.user):
            raise PermissionDenied()
        # if the payment has been completed, redirect to the success page
        if self.object.status == Payment.STATUS_PAID:
            return redirect(
                reverse('wechange-payments:payment-success',
                        kwargs={'pk': self.object.pk}))
        elif self.object.status not in [
                Payment.STATUS_STARTED,
                Payment.STATUS_COMPLETED_BUT_UNCONFIRMED
        ]:
            messages.error(
                self.request,
                str(_('This payment session has expired.')) + ' ' + str(
                    _('Please try again or contact our support for assistance!'
                      )))
            # redirect user to the payment form they were coming from
            if Subscription.get_active_for_user(self.request.user):
                return redirect('wechange-payments:payment-update')
            else:
                return redirect('wechange-payments:payment')
        return super(PaymentProcessView,
                     self).dispatch(request, *args, **kwargs)
Ejemplo n.º 6
0
    def dispatch(self, request, *args, **kwargs):
        if not self.request.user.is_authenticated:
            return super(PaymentInfosView,
                         self).dispatch(request, *args, **kwargs)

        self.current_subscription = Subscription.get_current_for_user(
            self.request.user)
        self.last_payment = None
        if self.current_subscription:
            self.last_payment = self.current_subscription.last_payment
        self.waiting_subscription = Subscription.get_waiting_for_user(
            self.request.user)
        self.subscription = self.waiting_subscription or self.current_subscription
        if not self.subscription:
            return redirect('wechange-payments:payment')
        return super(PaymentInfosView, self).dispatch(request, *args, **kwargs)
Ejemplo n.º 7
0
def cancel_subscription(user):
    """ Cancels the currently active or waiting subscription for a user """
    subscription = Subscription.get_current_for_user(user)
    subscription.state = Subscription.STATE_1_CANCELLED_BUT_ACTIVE
    subscription.cancelled = now()
    subscription.save()
    send_payment_event_payment_email(subscription.last_payment,
                                     PAYMENT_EVENT_SUBSCRIPTION_TERMINATED)
    return True
Ejemplo n.º 8
0
 def dispatch(self, request, *args, **kwargs):
     if not self.request.user.is_authenticated:
         return super(SuspendedSubscriptionView,
                      self).dispatch(request, *args, **kwargs)
     self.suspended_subscription = Subscription.get_suspended_for_user(
         self.request.user)
     if not self.suspended_subscription:
         return redirect('wechange-payments:overview')
     return super(SuspendedSubscriptionView,
                  self).dispatch(request, *args, **kwargs)
Ejemplo n.º 9
0
    def dispatch(self, request, *args, **kwargs):
        if not self.request.user.is_authenticated:
            return super(CancelSubscriptionView,
                         self).dispatch(request, *args, **kwargs)

        current_subscription = Subscription.get_active_for_user(
            self.request.user)
        if not current_subscription:
            return redirect('wechange-payments:overview')
        return super(CancelSubscriptionView,
                     self).dispatch(request, *args, **kwargs)
Ejemplo n.º 10
0
def subscription_change_amount(request):
    """ A user-based view, that is used only by the plattform itself, used to make the 
        first payment for the start of a subscription. Will not allow making a payment 
        while another subscription is still active! """
    if not request.method=='POST':
        return HttpResponseNotAllowed(['POST'])
    if not request.user.is_authenticated:
        return HttpResponseForbidden('You must be logged in to do that!')
    
    # if the user has no active or waiting sub, we cannot change the amount of it
    active_sub = Subscription.get_active_for_user(request.user)
    waiting_sub = Subscription.get_waiting_for_user(request.user)
    if not active_sub and not waiting_sub:
        return JsonResponse({'error': _('You do not currently have a subscription!')}, status=500)
    
    # sanity check
    if active_sub and waiting_sub:
        logger.error('Critical: Sanity check for user subscription failed! User has both an active and a queued subscription!', extra={'user': request.user.email})
        return JsonResponse({'error': _('An error occured, and you cannot change your subscription amount at this time. Please contact our support!')}, status=500)
    
    # validate amount
    amount_or_error_response = _get_validated_amount(request.POST.get('amount', None))
    if isinstance(amount_or_error_response, JsonResponse):
        return amount_or_error_response
    amount = amount_or_error_response
    
    subscription = active_sub or waiting_sub
    if subscription.amount == amount:
        messages.success(request, _('The amount you selected was the same amount as before, so we did not change anything!'))
    else:
        success = change_subscription_amount(subscription, amount)
        if not success:
            return JsonResponse({'error': _('Your subscription amount could not be changed because of an unexpected error. Please contact our support!')}, status=500)
        messages.success(request, _('Your changes have been saved! From now on your new chosen contribution amount will be paid. Thank you very much for your support!'))
    
    redirect_url = reverse('wechange_payments:my-subscription')
    data = {
        'redirect_to': redirect_url
    }
    return JsonResponse(data)
Ejemplo n.º 11
0
    def get_context_data(self, *args, **kwargs):
        context = super(PaymentView, self).get_context_data(*args, **kwargs)

        initial = {}
        if settings.DEBUG:
            initial = TEST_DATA_SEPA_PAYMENT_FORM
        if self.request.user.first_name:
            initial['first_name'] = self.request.user.first_name
        if self.request.user.last_name:
            initial['last_name'] = self.request.user.last_name
        initial['email'] = self.request.user.email
        form = PaymentsForm(initial=initial)

        current_sub = Subscription.get_current_for_user(self.request.user)
        waiting_sub = Subscription.get_waiting_for_user(self.request.user)

        context.update({
            'form':
            form,
            'displayed_subscription':
            waiting_sub or current_sub or None,
        })
        return context
Ejemplo n.º 12
0
    def get_redirect_url(self, *args, **kwargs):
        suspended = Subscription.get_suspended_for_user(self.request.user)
        if suspended:
            return reverse('wechange-payments:suspended-subscription')

        non_terminated_states = [
            Subscription.STATE_1_CANCELLED_BUT_ACTIVE,
            Subscription.STATE_2_ACTIVE,
        ]
        if settings.PAYMENTS_POSTPONED_PAYMENTS_IMPLEMENTED:
            non_terminated_states += [
                Subscription.STATE_3_WAITING_TO_BECOME_ACTIVE,
            ]
        subscription = get_object_or_None(Subscription,
                                          user=self.request.user,
                                          state__in=non_terminated_states)
        if not subscription or subscription.state in Subscription.ALLOWED_TO_MAKE_NEW_SUBSCRIPTION_STATES:
            return reverse('wechange-payments:payment')
        else:
            return reverse('wechange-payments:my-subscription')
Ejemplo n.º 13
0
    def dispatch(self, request, *args, **kwargs):
        if not self.request.user.is_authenticated:
            return super(PaymentView, self).dispatch(request, *args, **kwargs)

        allow_active_subscription = kwargs.get('allow_active_subscription',
                                               False)
        if not allow_active_subscription:
            active_subscription = Subscription.get_active_for_user(
                self.request.user)
            if active_subscription:
                return redirect('wechange-payments:my-subscription')
        else:
            kwargs.pop('allow_active_subscription')

        processing_payment = get_object_or_None(
            Payment,
            user=request.user,
            status=Payment.STATUS_COMPLETED_BUT_UNCONFIRMED)
        if processing_payment:
            return redirect(
                reverse('wechange-payments:payment-process',
                        kwargs={'pk': processing_payment.pk}))
        return super(PaymentView, self).dispatch(request, *args, **kwargs)
Ejemplo n.º 14
0
def make_payment(request, on_success_func=None, make_postponed=False):
    """ A non-user-based payment API function, that can be used for anonymous (or user-based),
        one-time donations.
        
        @param on_success_func: The function that should be called on success, or None.
            In case we use a payment method that uses a vendor-step, we will get a postback
            for a success, so we don't need a success function here in that case.
        @param make_postponed: If True, make a call that only authorizes the payment, but does
            not yet process it. """
    
    if not request.method=='POST':
        return HttpResponseNotAllowed(['POST'])
    
    backend = get_backend()
    params = request.POST.copy()
    user = request.user if request.user.is_authenticated else None
    
    # validate amount
    amount_or_error_response = _get_validated_amount(params['amount'])
    if isinstance(amount_or_error_response, JsonResponse):
        return amount_or_error_response
    
    payment_type = params.get('payment_type', None)
    error = 'Payment Type "%s" is not supported!' % payment_type 
    if payment_type in settings.PAYMENTS_ACCEPTED_PAYMENT_METHODS:
        # check for valid form
        form = PaymentsForm(request.POST)
        if not payment_type == PAYMENT_TYPE_DIRECT_DEBIT:
            for field_name in ['iban', 'bic', 'account_holder']:
                # remove the SEPA fields for any other payment method so validation won't interfere
                del form.fields[field_name]
        if not form.is_valid():
            return JsonResponse({'error': _('Please correct the errors in the highlighted fields!'), 'field_errors': form.errors}, status=500)

        # get cleaned compact IBAN and BIC out of custom form cleaning
        if payment_type == PAYMENT_TYPE_DIRECT_DEBIT:
            params['iban'] = form.cleaned_data['iban']
            params['bic'] = form.cleaned_data['bic']
                
        # remove `organisation` from params if `is_organisation` was not checked
        if not params.get('is_organisation', False) and 'organisation' in params:
            del params['organisation']
            
        # check for complete parameter set
        missing_params = backend.check_missing_params(params, payment_type)
        if missing_params:
            return JsonResponse({'error': _('Please fill out all of the missing fields!'), 'missing_parameters': missing_params}, status=500)
        
        # safety catch: if we don't have the postponed flag (i.e. we are not voluntarily updating
        # the current payment with a new one), and we have an active subscription, block the payment
        if Subscription.get_active_for_user(user) and not make_postponed:
            return JsonResponse({'error': _('You currently already have an active subscription!')}, status=500)
        
        # if postponed waiting payments are not implemented, we just make a non-postponed payment
        # which will replace the current one
        if not settings.PAYMENTS_POSTPONED_PAYMENTS_IMPLEMENTED:
            make_postponed = False
        
        if payment_type == PAYMENT_TYPE_DIRECT_DEBIT:
            payment, error = backend.make_sepa_payment(params, user=user, make_postponed=make_postponed)
        elif payment_type == PAYMENT_TYPE_CREDIT_CARD:
            payment, error = backend.make_creditcard_payment(params, user=user, make_postponed=make_postponed)
        elif payment_type == PAYMENT_TYPE_PAYPAL:
            payment, error = backend.make_paypal_payment(params, user=user, make_postponed=make_postponed)
            
        if payment is not None and on_success_func is not None:
            return on_success_func(payment)
            
    if payment is None:
        # special error cases:
        # 126: Invalid bank info
        if payment_type == PAYMENT_TYPE_DIRECT_DEBIT and error.endswith('(126)'):
            return JsonResponse({'error': _('The entered payment information is invalid. Please make sure you entered everything correctly!'), 'missing_parameters': ['iban', 'bic']}, status=500)
        return JsonResponse({'error': error}, status=500)
    
    data = {}
    if 'redirect_to' in payment.extra_data:
        data.update({
            'redirect_to': payment.extra_data['redirect_to'],
            'redirect_in_popup': True,
        })
    return JsonResponse(data)
Ejemplo n.º 15
0
def create_subscription_for_payment(payment):
    """ Creates the subscription object for a user after the initial payment 
        for the subscription has completed successfully. 
        
        This handles all state changes of current or waiting subscriptions, depending 
        on the states of the existing subscriptions. All entry API functions are safely
        gate-kept, so we can assume here that this was well-checked for states.
        I.e. if this call comes to pass, and the user may create a new subscription.
        Should they have an active subscription, they must have called update_payment, 
        so cancel the current subscription, and create a new, waiting one (in that
        case the payment will have been made to be postponed).
    """

    try:
        user = payment.user
        active_sub = Subscription.get_active_for_user(user)
        cancelled_sub = Subscription.get_canceled_for_user(user)
        suspended_sub = Subscription.get_suspended_for_user(user)

        subscription = Subscription(
            user=payment.user,
            reference_payment=payment,
            amount=payment.amount,
            last_payment=payment,
        )

        # the numbers refer to the state-change cases in `Subscription`'s docstring!
        with transaction.atomic():

            mail_event = None
            # terminate any failed suspended subscriptions
            if suspended_sub:
                suspended_sub.state = Subscription.STATE_0_TERMINATED
                suspended_sub.terminated = now()
                suspended_sub.save()

            if not active_sub and not cancelled_sub:
                # 1. (new subscription)
                subscription.set_next_due_date(now().date())
                subscription.state = Subscription.STATE_2_ACTIVE
                mail_event = PAYMENT_EVENT_NEW_SUBSCRIPTION_CREATED
            elif active_sub or cancelled_sub:
                # 2 and 3.. (updated payment infos, new sub becomes active sub, current one is terminated,
                #    remaining time is added to new sub)
                replaced_sub = active_sub or cancelled_sub
                subscription.set_next_due_date(replaced_sub.next_due_date)
                subscription.state = Subscription.STATE_2_ACTIVE
                replaced_sub.state = Subscription.STATE_0_TERMINATED
                replaced_sub.cancelled = now()
                replaced_sub.terminated = now()
                replaced_sub.save()
                mail_event = PAYMENT_EVENT_NEW_REPLACEMENT_SUBSCRIPTION_CREATED

            else:
                logger.critical(
                    'Payments: "Unreachable" case reached for subscription state situation for a user! Could not save the user\'s new subscription! This has to be checked out manually!.',
                    extra={
                        'payment-id': payment.id,
                        'payment': payment,
                        'user': user
                    })

            # set the last pre-notification date to now, so for the next due payment we know it has not been sent
            subscription.last_pre_notification_at = now()
            subscription.save()

            payment.subscription = subscription
            payment.save()

            if mail_event:
                send_payment_event_payment_email(payment, mail_event)

            logger.info(
                'Payments: Successfully created a new subscription for a user.',
                extra={
                    'payment-id': payment.id,
                    'payment': payment,
                    'user': user
                })
    except Exception as e:
        logger.error(
            'Payments: Critical! A user made a successful payment, but there was an error while creating his subscription! Find out what happened, create a subscription for them, and contact them!',
            extra={
                'payment-id': payment.id,
                'payment': payment,
                'user': payment.user,
                'exception': e
            })
        raise

    return subscription
Ejemplo n.º 16
0
def process_due_subscription_payments():
    """ Main loop for subscription management. Checks all subscriptions for
        validity, terminates expired subscriptions, activates waiting subscriptions
        and triggers payments on active subscriptions where a payment is due. """

    ended_subscriptions = 0
    # check for terminating subs, and activate valid waiting subs, afterwards all active subs will be valid
    for ending_sub in Subscription.objects.filter(
            state=Subscription.STATE_1_CANCELLED_BUT_ACTIVE):
        try:
            ended = ending_sub.validate_state_and_cycle()
            if ended:
                ended_subscriptions += 1
        except Exception as e:
            logger.error(
                'Payments: Exception during the call validate_state_and_cycle on a subscription! This is critical and needs to be fixed!',
                extra={
                    'user': ending_sub.user,
                    'subscription': ending_sub,
                    'exception': e
                })
            if settings.DEBUG:
                raise
            return (ended_subscriptions, 0)

    # switch for not-implemented postponed subscriptions
    if settings.PAYMENTS_POSTPONED_PAYMENTS_IMPLEMENTED:
        # for each waiting sub, check to see if the user has neither an active or canceled sub
        # (i.e. their canceled sub was just terminated)
        for waiting_sub in Subscription.objects.filter(
                state=Subscription.STATE_3_WAITING_TO_BECOME_ACTIVE):
            active_or_canceled_sub = Subscription.get_current_for_user(
                waiting_sub.user)
            if not active_or_canceled_sub:
                # if there were no active subs, activate the waiting sub. its due date should have
                # been set to the last sub at creation time.
                waiting_sub.state = Subscription.STATE_2_ACTIVE
                waiting_sub.save()

    booked_subscriptions = 0
    # check active subscriptions for due pre-notifications or payments
    for active_sub in Subscription.objects.filter(
            state=Subscription.STATE_2_ACTIVE):
        # check if pre-notification should be sent for SEPA subscription
        if active_sub.check_pre_notification_due(
        ) and active_sub.user.is_active:
            try:
                send_pre_notification_for_subscription_payment(active_sub)
            except Exception as e:
                logger.error(
                    'Payments: Exception while trying to send a pre-notification for a subscription that has its notification due!',
                    extra={
                        'user': active_sub.user,
                        'subscription': active_sub,
                        'exception': e
                    })
                if settings.DEBUG:
                    raise

        # if an active subscription has its payment is due trigger a new payment on it
        if active_sub.check_payment_due() and active_sub.user.is_active:
            try:
                if active_sub.has_pending_payment():
                    # if a  subscription's recurring payment is still pending, we do not book another payment
                    if active_sub.last_payment.last_action_at < (
                            now() - timedelta(days=1)):
                        # but if the payment has been made over 1 day ago and is still due, we trigger a critical alert!
                        extra = {
                            'user':
                            active_sub.user,
                            'subscription':
                            active_sub,
                            'internal_transaction_id':
                            str(active_sub.last_payment.internal_transaction_id
                                )
                        }
                        logger.critical(
                            'Payments: A recurring payment that has been started over 1 day ago still has its status at pending and has not received a postback! Only postbacks can set payments to not pending. The subscription is therefore also pending and is basically frozen. This needs to be investigated manually!',
                            extra=extra)
                    continue
                payment_or_none = book_next_subscription_payment(active_sub)
                if payment_or_none is not None:
                    booked_subscriptions += 1
            except Exception as e:
                logger.error(
                    'Payments: Exception while trying to book the next subscruption payment for a due subscription!',
                    extra={
                        'user': active_sub.user,
                        'subscription': active_sub,
                        'exception': e
                    })
                if settings.DEBUG:
                    raise

    return (ended_subscriptions, booked_subscriptions)
Ejemplo n.º 17
0
    def test_1_full_sepa_payment_and_invoice(self):
        # create a new user. this user will keep his subscription
        self.loyal_user = self._create_user(username='******')
        loyal_amount = 7.0
        self.client.force_login(self.loyal_user)
        loyal_data = copy(TEST_DATA_SEPA_PAYMENT_FORM)
        loyal_data['amount'] = loyal_amount
        response = self._make_sepa_payment(loyal_data)
        self.assertEqual(response.status_code, 200,
                         'Payment response redirects')

        loyal_subscription = get_object_or_None(Subscription,
                                                user=self.loyal_user)
        payment = get_object_or_None(Payment, user=self.loyal_user)
        self.assertIsNotNone(loyal_subscription,
                             'Subscription created after payment')
        self.assertIsNotNone(payment, 'Payment created afert payment')
        self.assertEqual(
            response.json()['redirect_to'],
            reverse('wechange-payments:payment-success',
                    kwargs={'pk': payment.pk}),
            'Redirected to correct success page')
        self.assertEqual(loyal_subscription.state, Subscription.STATE_2_ACTIVE,
                         'Subscription is active')
        self.assertEqual(payment.type, PAYMENT_TYPE_DIRECT_DEBIT,
                         'Payment type correct')
        self.assertEqual(payment.status, Payment.STATUS_PAID,
                         'Payment status is paid')
        self.assertEqual(loyal_subscription.amount, loyal_data['amount'],
                         'Subscription amount correct')
        self.assertEqual(payment.amount, loyal_subscription.amount,
                         'Payment amount is the same as its subscription')
        self.assertEqual(payment.subscription, loyal_subscription,
                         'Payment references subscription')
        self.assertEqual(loyal_subscription.reference_payment, payment,
                         'Subscription references payment')
        self.assertEqual(loyal_subscription.last_payment, payment,
                         'Subscription references payment as most recent')
        self.assertGreater(loyal_subscription.get_next_payment_date(),
                           now().date(), 'Subscription due date is in future')
        self.assertFalse(loyal_subscription.check_payment_due(),
                         'Subscription is not due')
        self.assertEqual(loyal_subscription,
                         Subscription.get_active_for_user(self.loyal_user),
                         'User has an "active" subscription after payment')
        self.assertEqual(loyal_subscription,
                         Subscription.get_current_for_user(self.loyal_user),
                         'User has a "current" subscription after payment')

        # TODO: test transaction log generation
        logger.warn('TODO: transaction log assertions')

        # manually trigger the invoice generation (it wasn't done automatically as hooks are disabled for testing)
        invoice_backend = get_invoice_backend()
        invoice_backend.create_invoice_for_payment(payment, threaded=False)
        invoice = get_object_or_None(Invoice, user=self.loyal_user)
        self.assertIsNotNone(invoice, 'Invoice created after payment')
        self.assertEqual(invoice.state, Invoice.STATE_3_DOWNLOADED,
                         'Invoice was completed')
        self.assertTrue(invoice.is_ready, 'Invoice ready flag set')
        self.assertIsNotNone(invoice.file,
                             'Invoice file downloaded and available')
        self.assertEqual(invoice.payment, payment,
                         'Invoice references payment')

        # for a new user, make a payment. this user will make a one-time payment and then cancel his subscription
        self.disloyal_user = self._create_user(username='******')
        self.client.force_login(self.disloyal_user)
        disloyal_data = copy(TEST_DATA_SEPA_PAYMENT_FORM)
        disloyal_amount = 1.0
        disloyal_data['amount'] = disloyal_amount
        response = self._make_sepa_payment(disloyal_data)
        disloyal_subscription = get_object_or_None(Subscription,
                                                   user=self.disloyal_user)
        self.assertEqual(disloyal_subscription,
                         Subscription.get_active_for_user(self.disloyal_user),
                         'User has an "active" subscription after payment')
        # cancel the subscription
        response = self.client.post(
            reverse('wechange-payments:cancel-subscription'))
        # reload subscription from DB
        disloyal_subscription = get_object_or_None(Subscription,
                                                   user=self.disloyal_user)
        self.assertEqual(
            disloyal_subscription.state,
            Subscription.STATE_1_CANCELLED_BUT_ACTIVE,
            'Active subscription switched to canceled state after cancellation'
        )
        self.assertIsNone(
            Subscription.get_active_for_user(self.disloyal_user),
            'User with canceled sub has no "active" subscription')
        self.assertEqual(
            disloyal_subscription,
            Subscription.get_current_for_user(self.disloyal_user),
            'User with canceled sub has a "current" subscription')

        # ------ run subscription processing ------
        process_due_subscription_payments()

        # reload subscriptions from DB
        loyal_subscription = get_object_or_None(Subscription,
                                                user=self.loyal_user)
        disloyal_subscription = get_object_or_None(Subscription,
                                                   user=self.disloyal_user)
        # no changes in subscription states (because no time has passed and subscriptions weren't due)
        self.assertEqual(
            loyal_subscription.state, Subscription.STATE_2_ACTIVE,
            'Active subscription kept its state after daily subscription processing'
        )
        self.assertEqual(
            disloyal_subscription.state,
            Subscription.STATE_1_CANCELLED_BUT_ACTIVE,
            'Canceled not-due subscription kept its state after daily subscription processing'
        )
        self.assertEqual(
            loyal_subscription,
            Subscription.get_active_for_user(self.loyal_user),
            'User has an "active" subscription after subscription processing with not-due subscriptions'
        )
        self.assertEqual(
            loyal_subscription,
            Subscription.get_current_for_user(self.loyal_user),
            'User has a "current" subscription after subscription processing with not-due subscriptions'
        )
        self.assertIsNone(
            Subscription.get_active_for_user(self.disloyal_user),
            'User with canceled sub has no "active" subscription after subscription processing with not-due subscriptions'
        )
        self.assertEqual(
            disloyal_subscription,
            Subscription.get_current_for_user(self.disloyal_user),
            'User with canceled sub has a "current" subscription after subscription processing with not-due subscriptions'
        )
        # no changes in payments should have happened
        loyal_payments = Payment.objects.filter(user=self.loyal_user)
        disloyal_payments = Payment.objects.filter(user=self.disloyal_user)
        self.assertEqual(
            loyal_payments.count(), 1,
            'No payments were made after subscription processing with not-due subscriptions'
        )
        self.assertEqual(
            disloyal_payments.count(), 1,
            'No payments were made after subscription processing with not-due subscriptions'
        )
        loyal_payment = loyal_payments[0]
        disloyal_payment = disloyal_payments[0]

        # ------ time passes ------
        # fake the subscription and payment datetimes so that they magically happened 32 days ago
        for sub in [loyal_subscription, disloyal_subscription]:
            sub.created = sub.created - timedelta(days=32)
            sub.set_next_due_date(sub.created)
            sub.save()
        for pay in [loyal_payment, disloyal_payment]:
            pay.completed_at = pay.completed_at - timedelta(days=32)
            pay.last_action_at = pay.completed_at
            pay.save()
        # reload subscriptions from DB
        loyal_subscription = get_object_or_None(Subscription,
                                                user=self.loyal_user)
        disloyal_subscription = get_object_or_None(Subscription,
                                                   user=self.disloyal_user)
        # make sure our datetime-faking worked and both subs are due
        self.assertTrue(loyal_subscription.check_payment_due(),
                        '32 days old Subscription is due for payment')
        self.assertFalse(
            disloyal_subscription.check_payment_due(),
            '32 days old cancelled Subscription is not due for payment')
        self.assertTrue(
            disloyal_subscription.check_termination_due(),
            '32 days old cancelled Subscription is due for termination')

        # ------ run subscription processing ------
        time_before_subscription_processing = now()
        process_due_subscription_payments()
        # reload subscriptions from DB
        loyal_subscription = get_object_or_None(Subscription,
                                                user=self.loyal_user)
        disloyal_subscription = get_object_or_None(Subscription,
                                                   user=self.disloyal_user)

        # canceled sub should now be terminated, terminated user should not have a subscription
        self.assertEqual(
            disloyal_subscription.state, Subscription.STATE_0_TERMINATED,
            'Canceled due subscription was terminated after daily subscription processing'
        )
        self.assertIsNone(
            Subscription.get_active_for_user(self.disloyal_user),
            'User with terminated sub has no "active" subscription')
        self.assertIsNone(
            Subscription.get_current_for_user(self.disloyal_user),
            'User with terminated sub has no "current" subscription')
        # loyal user sub should still be active, user should have a subscription, due date should be future
        self.assertEqual(
            loyal_subscription.state, Subscription.STATE_2_ACTIVE,
            'Due active subscription kept its state after being renews daily subscription processing'
        )
        self.assertEqual(
            loyal_subscription,
            Subscription.get_active_for_user(self.loyal_user),
            'User has an "active" subscription after subscription processing with due subscriptions'
        )
        self.assertEqual(
            loyal_subscription,
            Subscription.get_current_for_user(self.loyal_user),
            'User has a "current" subscription after subscription processing with due subscriptions'
        )
        self.assertGreater(
            loyal_subscription.next_due_date,
            time_before_subscription_processing.date(),
            'Due active subscription kept its due date in the past after subscription processing'
        )

        # loyal user should have a new payment made (a recurrent one), disloyal one should not
        loyal_payments = Payment.objects.filter(
            user=self.loyal_user).order_by('-last_action_at')
        disloyal_payments = Payment.objects.filter(
            user=self.disloyal_user).order_by('-last_action_at')
        self.assertEqual(
            loyal_payments.count(), 2,
            'No payments were made after subscription processing with due active subscriptions'
        )
        self.assertEqual(
            disloyal_payments.count(), 1,
            'No payments were made after subscription processing with due canceled subscriptions'
        )
        recurrent_payment = loyal_payments[0]

        # the new payment should be just made, paid, not be the reference payment, refer to sub, amount same
        self.assertGreater(
            recurrent_payment.completed_at,
            time_before_subscription_processing,
            'We identified the monthly recurrent payment correctly for this test'
        )
        self.assertEqual(
            recurrent_payment.status, Payment.STATUS_PAID,
            'The monthly recurrent payment was successfully paid')
        self.assertFalse(
            recurrent_payment.is_reference_payment,
            'The monthly recurrent payment is not the reference payment')
        self.assertEqual(
            recurrent_payment.subscription, loyal_subscription,
            'The monthly recurrent payment references the correct subscription'
        )
        self.assertEqual(
            recurrent_payment.amount, loyal_amount,
            'The amount for the monthly recurrent payment is correct')
        self.assertEqual(
            recurrent_payment.amount, loyal_subscription.amount,
            'The amount for the monthly recurrent payment is the same as for its subscription'
        )

        # a user with an active subscription should not be able to make another one
        self.client.force_login(self.loyal_user)
        logger.warn('TODO: double-concurrent subscription making fail test!')

        # even if somehow the subscription date got wrongfully set back in time,
        # no payment should be able to be made because of safety checks with recently made payments
        logger.warn(
            'TODO: faked-subscription-date failsafe test that prevents payments'
        )