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 10KB

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