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
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
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), })
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), })
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)
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)