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

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