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.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  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 Default(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. currency=stockrecord.price_currency,
  100. excl_tax=stockrecord.price_excl_tax,
  101. tax=D('0.00'))
  102. class FixedRateTax(object):
  103. """
  104. Prices are the same as the price_excl_tax field on the
  105. stockrecord with zero tax.
  106. """
  107. rate = D('0.20')
  108. def pricing_policy(self, product, stockrecord):
  109. if not stockrecord:
  110. return prices.Unavailable()
  111. return prices.FixedPrice(
  112. currency=stockrecord.price_currency,
  113. excl_tax=stockrecord.price_excl_tax,
  114. tax=stockrecord.price_excl_tax * self.rate)
  115. class DeferredTax(object):
  116. """
  117. For when taxes aren't known until the shipping details are entered. Like
  118. in the USA
  119. """
  120. def pricing_policy(self, product, stockrecord):
  121. if not stockrecord:
  122. return prices.Unavailable()
  123. return prices.FixedPrice(
  124. currency=stockrecord.price_currency,
  125. excl_tax=stockrecord.price_excl_tax)
  126. # Example strategy composed of above mixins. For real projects, it's likely
  127. # you'll want to use a different pricing mixin as you'll probably want to
  128. # charge tax!
  129. class Default(UseFirstStockRecord, StockRequired, NoTax, Structured):
  130. """
  131. Default stock/price strategy that uses the first found stockrecord for a
  132. product, ensures that stock is available (unless the product class
  133. indicates that we don't need to track stock) and charges zero tax.
  134. """
  135. class US(UseFirstStockRecord, StockRequired, DeferredTax, Structured):
  136. """
  137. Default strategy for the USA (just for testing really)
  138. """