Beispiel #1
0
def pay_transaction_view(request, transaction, expired=None):
    if expired:
        return render(request, 'transactions/expired_payment.html', {
            'document': transaction.document,
        })

    if transaction.state != Transaction.States.Initial:
        return render(
            request, 'transactions/complete_payment.html', {
                'transaction': transaction,
                'document': transaction.document,
                'fail_data': FAIL_CODES.get(transaction.fail_code)
            })

    payment_processor = transaction.payment_method.get_payment_processor()

    view = payment_processor.get_view(transaction, request)
    if not view or not transaction.can_be_consumed:
        return render(request, 'transactions/expired_payment.html', {
            'document': transaction.document,
        })

    transaction.last_access = timezone.now()
    transaction.save()

    try:
        return view(request)
    except NotImplementedError:
        raise Http404
Beispiel #2
0
def pay_transaction_view(request, transaction, expired=None):
    if expired:
        return render(request, 'transactions/expired_payment.html',
                      {
                          'document': transaction.document,
                      })

    if transaction.state != Transaction.States.Initial:
        return render(request, 'transactions/complete_payment.html',
                      {
                          'transaction': transaction,
                          'document': transaction.document,
                          'fail_data': FAIL_CODES.get(transaction.fail_code)
                      })

    payment_processor = transaction.payment_method.get_payment_processor()

    view = payment_processor.get_view(transaction, request)
    if not view or not transaction.can_be_consumed:
        return render(request, 'transactions/expired_payment.html',
                      {
                          'document': transaction.document,
                      })

    transaction.last_access = timezone.now()
    transaction.save()

    try:
        return view(request)
    except NotImplementedError:
        raise Http404
Beispiel #3
0
def complete_payment_view(request, transaction, expired=None):
    if transaction.state == transaction.States.Initial:
        payment_processor = get_instance(transaction.payment_processor)
        payment_processor.handle_transaction_response(transaction, request)

    if 'return_url' in request.GET:
        redirect_url = furl(request.GET['return_url']).add({
                                'transaction_uuid': transaction.uuid
                            }).url
        return HttpResponseRedirect(redirect_url)
    else:
        return render(request, 'transactions/complete_payment.html',
                      {
                          'transaction': transaction,
                          'document': transaction.document,
                          'fail_data': FAIL_CODES.get(transaction.fail_code),
                      })
Beispiel #4
0
def complete_payment_view(request, transaction, expired=None):
    if transaction.state == transaction.States.Initial:
        payment_processor = get_instance(transaction.payment_processor)
        payment_processor.handle_transaction_response(transaction, request)

    if 'return_url' in request.GET:
        redirect_url = six.moves.urllib.parse.unquote(
            furl(request.GET['return_url']).add(
                {
                    'transaction_uuid': transaction.uuid
                }
            ).url
        )
        return HttpResponseRedirect(redirect_url)
    else:
        return render(request, 'transactions/complete_payment.html',
                      {
                          'transaction': transaction,
                          'document': transaction.document,
                          'fail_data': FAIL_CODES.get(transaction.fail_code),
                      })
Beispiel #5
0
class Transaction(models.Model):
    amount = models.DecimalField(
        decimal_places=2,
        max_digits=12,
        validators=[MinValueValidator(Decimal('0.00'))])
    currency = models.CharField(choices=currencies,
                                max_length=4,
                                help_text='The currency used for billing.')

    class States:
        Initial = 'initial'
        Pending = 'pending'
        Settled = 'settled'
        Failed = 'failed'
        Canceled = 'canceled'
        Refunded = 'refunded'

        @classmethod
        def as_list(cls):
            return [
                getattr(cls, state) for state in vars(cls).keys()
                if state[0].isupper()
            ]

        @classmethod
        def as_choices(cls):
            return ((state, _(state.capitalize())) for state in cls.as_list())

    external_reference = models.CharField(max_length=256,
                                          null=True,
                                          blank=True)
    data = JSONField(default={}, null=True, blank=True)
    state = FSMField(max_length=8,
                     choices=States.as_choices(),
                     default=States.Initial)

    proforma = models.ForeignKey("Proforma", null=True, blank=True)
    invoice = models.ForeignKey("Invoice", null=True, blank=True)
    payment_method = models.ForeignKey('PaymentMethod')
    uuid = models.UUIDField(default=uuid.uuid4)
    valid_until = models.DateTimeField(null=True, blank=True)
    last_access = models.DateTimeField(null=True, blank=True)

    created_at = models.DateTimeField(default=timezone.now)
    updated_at = AutoDateTimeField(default=timezone.now)

    fail_code = models.CharField(choices=[(code, code)
                                          for code in FAIL_CODES.keys()],
                                 max_length=32,
                                 null=True,
                                 blank=True)
    refund_code = models.CharField(choices=[(code, code)
                                            for code in REFUND_CODES.keys()],
                                   max_length=32,
                                   null=True,
                                   blank=True)
    cancel_code = models.CharField(choices=[(code, code)
                                            for code in CANCEL_CODES.keys()],
                                   max_length=32,
                                   null=True,
                                   blank=True)

    @property
    def final_fields(self):
        fields = [
            'proforma', 'uuid', 'payment_method', 'amount', 'currency',
            'created_at'
        ]

        if self.invoice:
            fields.append('invoice')

        return fields

    def __init__(self, *args, **kwargs):
        self.form_class = kwargs.pop('form_class', None)

        super(Transaction, self).__init__(*args, **kwargs)

    @transition(field=state, source=States.Initial, target=States.Pending)
    def process(self):
        pass

    @transition(field=state,
                source=[States.Initial, States.Pending],
                target=States.Settled)
    def settle(self):
        pass

    @transition(field=state,
                source=[States.Initial, States.Pending],
                target=States.Canceled)
    def cancel(self,
               cancel_code='default',
               cancel_reason='Unknown cancel reason'):
        self.cancel_code = cancel_code
        logger.error(str(cancel_reason))

    @transition(field=state,
                source=[States.Initial, States.Pending],
                target=States.Failed)
    def fail(self, fail_code='default', fail_reason='Unknown fail reason'):
        self.fail_code = fail_code
        logger.error(str(fail_reason))

    @transition(field=state, source=States.Settled, target=States.Refunded)
    def refund(self,
               refund_code='default',
               refund_reason='Unknown refund reason'):
        self.refund_code = refund_code
        logger.error(str(refund_reason))

    def clean(self):
        document = self.document
        if not document:
            raise ValidationError(
                'The transaction must have at least one document '
                '(invoice or proforma).')

        if document.state == document.STATES.DRAFT:
            raise ValidationError(
                'The transaction must have a non-draft document '
                '(invoice or proforma).')

        if document.provider != self.provider:
            raise ValidationError(
                'Provider doesn\'t match with the one in documents.')

        if document.customer != self.customer:
            raise ValidationError(
                'Customer doesn\'t match with the one in documents.')

        if self.invoice and self.proforma:
            if self.invoice.proforma != self.proforma:
                raise ValidationError('Invoice and proforma are not related.')
        else:
            if self.invoice:
                self.proforma = self.invoice.proforma
            else:
                self.invoice = self.proforma.invoice

        # New transaction
        if not self.pk:
            if document.state != document.STATES.ISSUED:
                raise ValidationError(
                    'Transactions can only be created for issued documents.')

            if self.currency:
                if self.currency != self.document.transaction_currency:
                    raise ValidationError(
                        "Transaction currency is different from it's document's"
                        " transaction_currency.")
            else:
                self.currency = self.document.transaction_currency

            if (self.payment_method.allowed_currencies and self.currency
                    not in self.payment_method.allowed_currencies):
                raise ValidationError(
                    'Currency {} is not allowed by the payment method. '
                    'Allowed currencies are {}.'.format(
                        self.currency, self.payment_method.allowed_currencies))

            if self.amount:
                if self.amount != self.document.transaction_total:
                    raise ValidationError(
                        "Transaction amount is different from it's document's "
                        "transaction_total.")
            else:
                self.amount = self.document.transaction_total

            # We also check for settled because document pay transition might fail
            if self.document.transactions.filter(state__in=[
                    Transaction.States.Initial, Transaction.States.Pending,
                    Transaction.States.Settled
            ]).exists():
                raise ValidationError(
                    'There already are active transactions for the same '
                    'billing documents.')

    def full_clean(self, *args, **kwargs):
        # 'amount' and 'currency' are handled in our clean method
        kwargs['exclude'] = kwargs.get('exclude', []) + ['currency', 'amount']
        super(Transaction, self).full_clean(*args, **kwargs)

        # this assumes that nobody calls clean and then modifies this object
        # without calling clean again
        self.cleaned = True

    @property
    def can_be_consumed(self):
        if self.valid_until and self.valid_until < timezone.now():
            return False

        if self.state != Transaction.States.Initial:
            return False

        return True

    @property
    def customer(self):
        return self.payment_method.customer

    @property
    def document(self):
        return self.invoice or self.proforma

    @property
    def provider(self):
        return self.document.provider

    @property
    def payment_processor(self):
        return self.payment_method.payment_processor

    def update_document_state(self):
        if (self.state == Transaction.States.Settled
                and self.document.state != self.document.STATES.PAID):
            self.document.pay()
            self.document.save()

    def __unicode__(self):
        return unicode(self.uuid)
Beispiel #6
0
class Transaction(AutoCleanModelMixin, models.Model):
    _provider = None

    amount = models.DecimalField(
        decimal_places=2,
        max_digits=12,
        validators=[MinValueValidator(Decimal('0.00'))])
    currency = models.CharField(choices=currencies,
                                max_length=4,
                                help_text='The currency used for billing.')

    class Meta:
        ordering = ['-id']

    class States:
        Initial = 'initial'
        Pending = 'pending'
        Settled = 'settled'
        Failed = 'failed'
        Canceled = 'canceled'
        Refunded = 'refunded'

        @classmethod
        def as_list(cls):
            return [
                getattr(cls, state) for state in vars(cls).keys()
                if state[0].isupper()
            ]

        @classmethod
        def as_choices(cls):
            return ((state, _(state.capitalize())) for state in cls.as_list())

    external_reference = models.CharField(max_length=256,
                                          null=True,
                                          blank=True)
    data = JSONField(default=dict,
                     null=True,
                     blank=True,
                     encoder=DjangoJSONEncoder)
    state = FSMField(max_length=8,
                     choices=States.as_choices(),
                     default=States.Initial)

    proforma = models.ForeignKey("BillingDocumentBase",
                                 null=True,
                                 blank=True,
                                 on_delete=models.SET_NULL,
                                 related_name='proforma_transactions')
    invoice = models.ForeignKey("BillingDocumentBase",
                                null=True,
                                blank=True,
                                on_delete=models.SET_NULL,
                                related_name='invoice_transactions')

    payment_method = models.ForeignKey('PaymentMethod',
                                       on_delete=models.PROTECT)
    uuid = models.UUIDField(default=uuid.uuid4)
    valid_until = models.DateTimeField(null=True, blank=True)
    last_access = models.DateTimeField(null=True, blank=True)

    created_at = models.DateTimeField(default=timezone.now)
    updated_at = AutoDateTimeField(default=timezone.now)

    fail_code = models.CharField(choices=[(code, code)
                                          for code in FAIL_CODES.keys()],
                                 max_length=64,
                                 null=True,
                                 blank=True)
    refund_code = models.CharField(choices=[(code, code)
                                            for code in REFUND_CODES.keys()],
                                   max_length=32,
                                   null=True,
                                   blank=True)
    cancel_code = models.CharField(choices=[(code, code)
                                            for code in CANCEL_CODES.keys()],
                                   max_length=32,
                                   null=True,
                                   blank=True)

    @property
    def final_fields(self):
        fields = [
            'proforma', 'invoice', 'uuid', 'payment_method', 'amount',
            'currency', 'created_at'
        ]

        return fields

    def __init__(self, *args, **kwargs):
        self.form_class = kwargs.pop('form_class', None)

        super(Transaction, self).__init__(*args, **kwargs)

    @transition(field=state, source=States.Initial, target=States.Pending)
    def process(self):
        pass

    @transition(field=state,
                source=[States.Initial, States.Pending],
                target=States.Settled)
    def settle(self):
        pass

    @transition(field=state,
                source=[States.Initial, States.Pending],
                target=States.Canceled)
    def cancel(self,
               cancel_code='default',
               cancel_reason='Unknown cancel reason'):
        self.cancel_code = cancel_code
        logger.error(str(cancel_reason))

    @transition(field=state,
                source=[States.Initial, States.Pending],
                target=States.Failed)
    def fail(self, fail_code='default', fail_reason='Unknown fail reason'):
        self.fail_code = fail_code
        logger.error(str(fail_reason))

    @transition(field=state, source=States.Settled, target=States.Refunded)
    def refund(self,
               refund_code='default',
               refund_reason='Unknown refund reason'):
        self.refund_code = refund_code
        logger.error(str(refund_reason))

    @transaction.atomic()
    def save(self, *args, **kwargs):
        if not self.pk:
            # Creating a new Transaction so we lock the DB rows for related billing documents and
            # transactions
            if self.proforma:
                Proforma.objects.select_for_update().filter(
                    pk=self.proforma.pk)
            elif self.invoice:
                Invoice.objects.select_for_update().filter(pk=self.invoice.pk)

            Transaction.objects.select_for_update().filter(
                Q(proforma=self.proforma) | Q(invoice=self.invoice))

        super(Transaction, self).save(*args, **kwargs)

    def clean(self):
        # Validate documents
        document = self.document
        if not document:
            raise ValidationError(
                'The transaction must have at least one billing document '
                '(invoice or proforma).')

        if document.state == document.STATES.DRAFT:
            raise ValidationError(
                'The transaction must have a non-draft billing document '
                '(invoice or proforma).')

        if self.invoice and self.proforma:
            if self.invoice.related_document != self.proforma:
                raise ValidationError('Invoice and proforma are not related.')
        else:
            if self.invoice:
                self.proforma = self.invoice.related_document
            else:
                self.invoice = self.proforma.related_document

        if document.customer != self.customer:
            raise ValidationError(
                'Customer doesn\'t match with the one in documents.')

        # New transaction
        if not self.pk:
            if document.state != document.STATES.ISSUED:
                raise ValidationError(
                    'Transactions can only be created for issued documents.')

            if self.currency:
                if self.currency != self.document.transaction_currency:
                    message = "Transaction currency is different from it's document's "\
                              "transaction_currency."
                    raise ValidationError(message)
            else:
                self.currency = self.document.transaction_currency

            if (self.payment_method.allowed_currencies and self.currency
                    not in self.payment_method.allowed_currencies):
                message = 'Currency {} is not allowed by the payment method. Allowed currencies ' \
                          'are {}.'.format(
                              self.currency, self.payment_method.allowed_currencies
                          )
                raise ValidationError(message)
            if self.amount:
                if self.amount > self.document.amount_to_be_charged_in_transaction_currency:
                    message = "Amount is greater than the amount that should be charged in order " \
                              "to pay the billing document."
                    raise ValidationError(message)
            else:
                self.amount = self.document.amount_to_be_charged_in_transaction_currency
        else:
            # clean final fields
            errors = {}
            for field in self.final_fields:
                old_value = self.initial_state.get(field)
                current_value = self.current_state.get(field)

                if old_value is not None and old_value != current_value:
                    errors[field] = 'This field may not be modified.'
            if errors:
                raise ValidationError(errors)

    def full_clean(self, *args, **kwargs):
        # 'amount' and 'currency' are handled in our clean method
        kwargs['exclude'] = kwargs.get('exclude', []) + ['currency', 'amount']

        super(Transaction, self).full_clean(*args, **kwargs)

    @property
    def can_be_consumed(self):
        if self.valid_until and self.valid_until < timezone.now():
            return False

        if self.state != Transaction.States.Initial:
            return False

        return True

    @property
    def customer(self):
        return self.payment_method.customer

    @property
    def document(self):
        return self.invoice or self.proforma

    @document.setter
    def document(self, value):
        if isinstance(value, Invoice):
            self.invoice = value
        elif isinstance(value, Proforma):
            self.proforma = value
        else:
            raise ValueError(
                'The provided document is not an invoice or a proforma.')

    @property
    def provider(self):
        return self._provider or self.document.provider

    @provider.setter
    def provider(self, provider):
        self._provider = provider

    @property
    def payment_processor(self):
        return self.payment_method.payment_processor

    def update_document_state(self):
        if (self.state == Transaction.States.Settled and
                not self.document.amount_to_be_charged_in_transaction_currency
                and self.document.state != self.document.STATES.PAID):
            self.document.pay()

    def __str__(self):
        return force_str(self.uuid)