You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

abstract_models.py 10.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. from decimal import Decimal
  2. from django.db import models
  3. from django.utils.translation import ugettext as _
  4. from django.conf import settings
  5. from oscar.core.compat import AUTH_USER_MODEL
  6. from oscar.core.utils import slugify
  7. from . import bankcards
  8. class AbstractTransaction(models.Model):
  9. """
  10. A transaction with a payment gateway
  11. For example:
  12. * A 'pre-auth' with a bankcard gateway
  13. * A 'settle' with a credit provider (see django-oscar-accounts)
  14. """
  15. source = models.ForeignKey(
  16. 'payment.Source', related_name='transactions',
  17. verbose_name=_("Source"))
  18. # We define some sample types but don't constrain txn_type to be one of
  19. # these as there will be domain-specific ones that we can't anticipate
  20. # here.
  21. AUTHORISE, DEBIT, REFUND = 'Authorise', 'Debit', 'Refund'
  22. txn_type = models.CharField(_("Type"), max_length=128, blank=True)
  23. amount = models.DecimalField(_("Amount"), decimal_places=2, max_digits=12)
  24. reference = models.CharField(_("Reference"), max_length=128, blank=True)
  25. status = models.CharField(_("Status"), max_length=128, blank=True)
  26. date_created = models.DateTimeField(_("Date Created"), auto_now_add=True)
  27. def __unicode__(self):
  28. return _(u"%(type)s of %(amount).2f") % {
  29. 'type': self.txn_type,
  30. 'amount': self.amount}
  31. class Meta:
  32. abstract = True
  33. verbose_name = _("Transaction")
  34. verbose_name_plural = _("Transactions")
  35. class AbstractSource(models.Model):
  36. """
  37. A source of payment for an order.
  38. This is normally a credit card which has been pre-authed for the order
  39. amount, but some applications will allow orders to be paid for using
  40. multiple sources such as cheque, credit accounts, gift cards. Each payment
  41. source will have its own entry.
  42. This source object tracks how much money has been authorised, debited and
  43. refunded, which is useful when payment takes place in multiple stages.
  44. """
  45. order = models.ForeignKey(
  46. 'order.Order', related_name='sources', verbose_name=_("Order"))
  47. source_type = models.ForeignKey(
  48. 'payment.SourceType', verbose_name=_("Source Type"))
  49. currency = models.CharField(
  50. _("Currency"), max_length=12, default=settings.OSCAR_DEFAULT_CURRENCY)
  51. # Track the various amounts associated with this source
  52. amount_allocated = models.DecimalField(
  53. _("Amount Allocated"), decimal_places=2, max_digits=12,
  54. default=Decimal('0.00'))
  55. amount_debited = models.DecimalField(
  56. _("Amount Debited"), decimal_places=2, max_digits=12,
  57. default=Decimal('0.00'))
  58. amount_refunded = models.DecimalField(
  59. _("Amount Refunded"), decimal_places=2, max_digits=12,
  60. default=Decimal('0.00'))
  61. # Reference number for this payment source. This is often used to look up
  62. # a transaction model for a particular payment partner.
  63. reference = models.CharField(_("Reference"), max_length=128, blank=True)
  64. # A customer-friendly label for the source, eg XXXX-XXXX-XXXX-1234
  65. label = models.CharField(_("Label"), max_length=128, blank=True)
  66. # A dictionary of submission data that is stored as part of the
  67. # checkout process, where we need to pass an instance of this class around
  68. submission_data = None
  69. # We keep a list of deferred transactions that are only actually saved when
  70. # the source is saved for the first time
  71. deferred_txns = None
  72. class Meta:
  73. abstract = True
  74. verbose_name = _("Source")
  75. verbose_name_plural = _("Sources")
  76. def __unicode__(self):
  77. description = _("Allocation of %(amount).2f from type %(type)s") % {
  78. 'amount': self.amount_allocated, 'type': self.source_type}
  79. if self.reference:
  80. description += _(" (reference: %s)") % self.reference
  81. return description
  82. def save(self, *args, **kwargs):
  83. super(AbstractSource, self).save(*args, **kwargs)
  84. if self.deferred_txns:
  85. for txn in self.deferred_txns:
  86. self._create_transaction(*txn)
  87. def create_deferred_transaction(self, txn_type, amount, reference=None,
  88. status=None):
  89. """
  90. Register the data for a transaction that can't be created yet due to FK
  91. constraints. This happens at checkout where create an payment source
  92. and a transaction but can't save them until the order model exists.
  93. """
  94. if self.deferred_txns is None:
  95. self.deferred_txns = []
  96. self.deferred_txns.append((txn_type, amount, reference, status))
  97. def _create_transaction(self, txn_type, amount, reference=None,
  98. status=None):
  99. AbstractTransaction.objects.create(
  100. source=self, txn_type=txn_type, amount=amount,
  101. reference=reference, status=status)
  102. # =======
  103. # Actions
  104. # =======
  105. def allocate(self, amount, reference=None, status=None):
  106. """
  107. Convenience method for ring-fencing money against this source
  108. """
  109. self.amount_allocated += amount
  110. self.save()
  111. self._create_transaction(
  112. AbstractTransaction.AUTHORISE, amount, reference, status)
  113. allocate.alters_data = True
  114. def debit(self, amount=None, reference=None, status=None):
  115. """
  116. Convenience method for recording debits against this source
  117. """
  118. if amount is None:
  119. amount = self.balance()
  120. self.amount_debited += amount
  121. self.save()
  122. self._create_transaction(
  123. AbstractTransaction.DEBIT, amount, reference, status)
  124. debit.alters_data = True
  125. def refund(self, amount, reference=None, status=None):
  126. """
  127. Convenience method for recording refunds against this source
  128. """
  129. self.amount_refunded += amount
  130. self.save()
  131. self._create_transaction(
  132. AbstractTransaction.REFUND, amount, reference, status)
  133. refund.alters_data = True
  134. # ==========
  135. # Properties
  136. # ==========
  137. @property
  138. def balance(self):
  139. """
  140. Return the balance of this source
  141. """
  142. return (self.amount_allocated - self.amount_debited +
  143. self.amount_refunded)
  144. @property
  145. def amount_available_for_refund(self):
  146. """
  147. Return the amount available to be refunded
  148. """
  149. return self.amount_debited - self.amount_refunded
  150. class AbstractSourceType(models.Model):
  151. """
  152. A type of payment source.
  153. This could be an external partner like PayPal or DataCash,
  154. or an internal source such as a managed account.
  155. """
  156. name = models.CharField(_("Name"), max_length=128)
  157. code = models.SlugField(
  158. _("Code"), max_length=128,
  159. help_text=_("This is used within forms to identify this source type"))
  160. class Meta:
  161. abstract = True
  162. verbose_name = _("Source Type")
  163. verbose_name_plural = _("Source Types")
  164. def __unicode__(self):
  165. return self.name
  166. def save(self, *args, **kwargs):
  167. if not self.code:
  168. self.code = slugify(self.name)
  169. super(AbstractSourceType, self).save(*args, **kwargs)
  170. class AbstractBankcard(models.Model):
  171. """
  172. Model representing a user's bankcard. This is used for two purposes:
  173. 1. The bankcard form will return an instance of this model that can be
  174. used with payment gateways. In this scenario, the instance will
  175. have additional attributes (start_date, issue_number, ccv) that
  176. payment gateways need but that we don't save.
  177. 2. To keep a record of a user's bankcards and allow them to be
  178. re-used. This is normally done using the 'partner reference'.
  179. """
  180. user = models.ForeignKey(AUTH_USER_MODEL, related_name='bankcards',
  181. verbose_name=_("User"))
  182. card_type = models.CharField(_("Card Type"), max_length=128)
  183. # Often you don't actually need the name on the bankcard
  184. name = models.CharField(_("Name"), max_length=255, blank=True)
  185. # We store an obfuscated version of the card number, just showing the last
  186. # 4 digits.
  187. number = models.CharField(_("Number"), max_length=32)
  188. # We store a date even though only the month is visible. Bankcards are
  189. # valid until the last day of the month.
  190. expiry_date = models.DateField(_("Expiry Date"))
  191. # For payment partners who are storing the full card details for us
  192. partner_reference = models.CharField(
  193. _("Partner Reference"), max_length=255, blank=True)
  194. # Temporary data not persisted to the DB
  195. start_date = None
  196. issue_number = None
  197. ccv = None
  198. def __unicode__(self):
  199. return _(u"%(card_type)s %(number)s (Expires: %(expiry)s)") % {
  200. 'card_type': self.card_type,
  201. 'number': self.number,
  202. 'expiry': self.expiry_month()}
  203. def __init__(self, *args, **kwargs):
  204. # Pop off the temporary data
  205. self.start_date = kwargs.pop('start_date', None)
  206. self.issue_number = kwargs.pop('issue_number', None)
  207. self.ccv = kwargs.pop('ccv', None)
  208. super(AbstractBankcard, self).__init__(*args, **kwargs)
  209. class Meta:
  210. abstract = True
  211. verbose_name = _("Bankcard")
  212. verbose_name_plural = _("Bankcards")
  213. def save(self, *args, **kwargs):
  214. if not self.number.startswith('X'):
  215. self.prepare_for_save()
  216. super(AbstractBankcard, self).save(*args, **kwargs)
  217. def prepare_for_save(self):
  218. # This is the first time this card instance is being saved. We
  219. # remove all sensitive data
  220. self.card_type = bankcards.bankcard_type(self.number)
  221. if self.card_type is None:
  222. self.card_type = 'Unknown card type'
  223. self.number = u"XXXX-XXXX-XXXX-%s" % self.number[-4:]
  224. self.start_date = self.issue_number = self.ccv = None
  225. @property
  226. def cvv(self):
  227. return self.ccv
  228. def start_month(self, format='%m/%y'):
  229. return self.start_date.strftime(format)
  230. def expiry_month(self, format='%m/%y'):
  231. return self.expiry_date.strftime(format)