Created
August 19, 2021 14:23
-
-
Save volgoweb/f180658fe7149f4a59739dd0780b2ef4 to your computer and use it in GitHub Desktop.
19_08_2021 Interview (Shared)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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