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

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