Skip to content

Instantly share code, notes, and snippets.

@volgoweb
Created August 19, 2021 14:23
Show Gist options
  • Save volgoweb/f180658fe7149f4a59739dd0780b2ef4 to your computer and use it in GitHub Desktop.
Save volgoweb/f180658fe7149f4a59739dd0780b2ef4 to your computer and use it in GitHub Desktop.
19_08_2021 Interview (Shared)
import uuid
from decimal import Decimal
from django.db import transaction
# send a request to PayPal.com to transfer money
from .services import transfer_money_via_paypal
class BillingAccount(models.Model):
balance = models.DecimalField(max_digits=10, decimal_places=2)
@transaction.atomic
def increment_balance(self, amount: Decimal):
# Specially here, we lock current row in DB to perform our operation
account = self.get_queryset().select_for_update().get()
account.balance = F('balance') + amount
account.save(update_fields=['balance'])
@transaction.atomic
def decrement_balance(self, amount: Decimal):
# Specially here, we lock current row in DB to perform our operation
account = self.get_queryset().select_for_update().get()
account.balance = F('balance') - amount
account.save(update_fields=['balance'])
class TransactionStatus(models.TextChoices):
PENDING = 'pending', _('Pending')
PROCESSING = 'processing', _('Processing')
DONE = 'done', _('Done')
ERRORED = 'errored', _('Errored')
class PaymentTransferOperation(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
payer = models.ForeignKey(BillingAccount, on_delete=models.CASCADE)
recipient = models.ForeignKey(BillingAccount, on_delete=models.CASCADE)
amount = models.DecimalField(max_digits=10, decimal_places=2)
status = models.CharField(
max_length=30,
choices=TransactionStatus.choices,
default=TransactionStatus.PENDING
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def set_status(self, status: TransactionStatus):
operation = self.get_queryset().select_for_update().get()
operation.status = status
operation.save(update_fields=['status'])
@cached_property
def get_payer_paypal_id(self):
return self.payer.user.paypal_id
@cached_property
def get_recipient_paypal_id(self):
return self.recipient.user.paypal_id
@property
def can_start_processing(self):
return self.status == TransactionStatus.PENDING
@property
def is_balance_eligable(self):
return self.payer.balance >= self.amount
def perform_transaction(self):
self.payer.decrement_balance(self.balance)
self.recipient.increment_balance(self.balance)
self.set_status(TransactionStatus.DONE)
def perform_payment(payment_operation: PaymentTransferOperation):
if not payment_operation.can_start_processing:
# Already in progress, skip processing
raise Exception("Payment is already in process")
if not payment_operation.is_balance_eligable:
# For sake of simplicity, we can have here a custom exceptions
payment_operation.set_status(TransactionStatus.ERRORED)
# logging
raise Exception(
"Couldn't perform operation, payer balance is smaller then amount to transfer")
payment_operation.set_status(TransactionStatus.PROCESSING)
try:
with transaction.atomic(durable=True):
transfer_money_via_paypal(
payer=payment_operation.get_payer_paypal_id(),
recipient=payment_operation.get_recipient_paypal_id(),
amount=payment_operation.amount,
)
payment_operation.perform_transaction()
# In theory paypal can throw an error, we must handle it and write an errored status
except (PaypalError, IntegrityError):
# Logging
payment_operation.set_status(TransactionStatus.ERRORED)
# All necessary test cases.
# Two only need implementation. Remaining test cases could be empty (only name).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment