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

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