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.

strategy.py 5.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. from collections import namedtuple
  2. from decimal import Decimal as D
  3. from . import availability, prices
  4. # a container for policies
  5. StockInfo = namedtuple('StockInfo', ['price', 'availability', 'stockrecord'])
  6. class Selector(object):
  7. """
  8. Responsible for returning the appropriate strategy class for a given
  9. user/session.
  10. This can be called in three ways:
  11. 1. Passing a request and user. This is for determining
  12. prices/availability for a normal user browsing the site.
  13. 2. Passing just the user. This is for offline processes that don't
  14. have a request instance but do know which user to determine prices for.
  15. 3. Passing nothing. This is for offline processes that don't
  16. correspond to a specific user. Eg, determining a price to store in
  17. Solr's index.
  18. """
  19. def strategy(self, request=None, user=None, **kwargs):
  20. # Default to the backwards-compatible strategy of picking the first
  21. # stockrecord.
  22. return US(request)
  23. class Base(object):
  24. """
  25. The interface for a strategy. Only has to implement the fetch method,
  26. which is responsible for returning a StockInfo instance.
  27. """
  28. def __init__(self, request=None):
  29. self.request = request
  30. self.user = None
  31. if request and request.user.is_authenticated():
  32. self.user = request.user
  33. def fetch(self, product, stockrecord=None):
  34. raise NotImplementedError(
  35. "A strategy class must define a fetch method "
  36. "for returning the availability and pricing "
  37. "information."
  38. )
  39. class Structured(Base):
  40. """
  41. An intermediate class which should be sufficient for most use cases
  42. """
  43. def fetch(self, product, stockrecord=None):
  44. if stockrecord is None:
  45. stockrecord = self.select_stockrecord(product)
  46. return StockInfo(
  47. price=self.pricing_policy(product, stockrecord),
  48. availability=self.availability_policy(product, stockrecord),
  49. stockrecord=stockrecord)
  50. def select_stockrecord(self, product):
  51. """
  52. Select the appropriate stockrecord to go with the passed product
  53. """
  54. raise NotImplementedError(
  55. "A structured strategy class must define a "
  56. "'select_stockrecord' method")
  57. def pricing_policy(self, product, stockrecord):
  58. """
  59. Return the appropriate pricing policy
  60. """
  61. raise NotImplementedError(
  62. "A structured strategy class must define a "
  63. "'pricing_policy' method")
  64. def availability_policy(self, product, stockrecord):
  65. """
  66. Return the appropriate availability policy
  67. """
  68. raise NotImplementedError(
  69. "A structured strategy class must define a "
  70. "'availability_policy' method")
  71. # Mixins - these can be used to construct the appropriate strategy class
  72. class UseFirstStockRecord(object):
  73. """
  74. Always use the first (normally only) stock record for a product
  75. """
  76. def select_stockrecord(self, product):
  77. try:
  78. return product.stockrecords.all()[0]
  79. except IndexError:
  80. return None
  81. class StockRequired(object):
  82. def availability_policy(self, product, stockrecord):
  83. if not stockrecord:
  84. return availability.Unavailable()
  85. if not product.get_product_class().track_stock:
  86. return availability.Available()
  87. else:
  88. return availability.StockRequired(
  89. stockrecord.net_stock_level)
  90. class NoTax(object):
  91. """
  92. Prices are the same as the price_excl_tax field on the
  93. stockrecord with zero tax.
  94. """
  95. def pricing_policy(self, product, stockrecord):
  96. if not stockrecord:
  97. return prices.Unavailable()
  98. return prices.FixedPrice(
  99. excl_tax=stockrecord.price_excl_tax,
  100. tax=D('0.00'))
  101. class FixedRateTax(object):
  102. """
  103. Prices are the same as the price_excl_tax field on the
  104. stockrecord with zero tax.
  105. """
  106. rate = D('0.20')
  107. def pricing_policy(self, product, stockrecord):
  108. if not stockrecord:
  109. return prices.Unavailable()
  110. return prices.FixedPrice(
  111. excl_tax=stockrecord.price_excl_tax,
  112. tax=stockrecord.price_excl_tax * self.rate)
  113. class DeferredTax(object):
  114. """
  115. For when taxes aren't known until the shipping details are entered. Like
  116. in the USA
  117. """
  118. def pricing_policy(self, product, stockrecord):
  119. if not stockrecord:
  120. return prices.Unavailable()
  121. return prices.FixedPrice(
  122. excl_tax=stockrecord.price_excl_tax)
  123. # Example strategy composed of above mixins. For real projects, it's likely
  124. # you'll want to use a different pricing mixin as you'll probably want to
  125. # charge tax!
  126. class Default(UseFirstStockRecord, StockRequired, NoTax, Structured):
  127. """
  128. Default stock/price strategy that uses the first found stockrecord for a
  129. product, ensures that stock is available (unless the product class
  130. indicates that we don't need to track stock) and charges zero tax.
  131. """
  132. class US(UseFirstStockRecord, StockRequired, DeferredTax, Structured):
  133. """
  134. Default strategy for the USA
  135. """