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)
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)
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')
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)
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)
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)
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
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)
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)
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)
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
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')
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)
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)
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
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)
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' )