Coverage for amazonorders/entity/order.py: 75.30%

166 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-14 16:09 +0000

1import logging 

2from datetime import datetime 

3from urllib.parse import urlparse 

4from urllib.parse import parse_qs 

5 

6from bs4 import BeautifulSoup 

7 

8from amazonorders.entity.recipient import Recipient 

9from amazonorders.entity.shipment import Shipment 

10from amazonorders.entity.item import Item 

11from amazonorders.session import BASE_URL 

12 

13__author__ = "Alex Laird" 

14__copyright__ = "Copyright 2024, Alex Laird" 

15__version__ = "0.0.4" 

16 

17logger = logging.getLogger(__name__) 

18 

19 

20class Order: 

21 def __init__(self, 

22 parsed, 

23 full_details=False, 

24 clone=None) -> None: 

25 self.parsed = parsed 

26 self.full_details = full_details 

27 

28 if clone: 

29 self.shipments = clone.shipments 

30 self.items = clone.items 

31 self.order_details_link = clone.order_details_link 

32 self.order_number = clone.order_number 

33 self.grand_total = clone.grand_total 

34 self.order_placed_date = clone.order_placed_date 

35 self.recipient = clone.recipient 

36 else: 

37 self.shipments = self._parse_shipments() 

38 self.items = self._parse_items() 

39 self.order_details_link = self._parse_order_details_link() 

40 self.order_number = self._parse_order_number() 

41 self.grand_total = self._parse_grand_total() 

42 self.order_placed_date = self._parse_order_placed_date() 

43 self.recipient = self._parse_recipient() 

44 

45 if self.full_details: 

46 self.items = self._parse_items() 

47 self.payment_method = self._parse_payment_method() 

48 self.payment_method_last_4 = self._parse_payment_method_last_4() 

49 self.subtotal = self._parse_subtotal() 

50 self.shipping_total = self._parse_shipping_total() 

51 self.subscription_discount = self._parse_subscription_discount() 

52 self.total_before_tax = self._parse_total_before_tax() 

53 self.estimated_tax = self._parse_estimated_tax() 

54 self.refund_total = self._parse_refund_total() 

55 self.order_shipped_date = self._parse_order_shipping_date() 

56 self.refund_completed_date = self._parse_refund_completed_date() 

57 

58 def __repr__(self) -> str: 

59 return "<Order #{}: \"{}\">".format(self.order_number, self.items) 

60 

61 def __str__(self) -> str: # pragma: no cover 

62 return "Order #{}: \"{}\"".format(self.order_number, self.items) 

63 

64 def _parse_shipments(self): 

65 return [Shipment(x, self) for x in self.parsed.find_all("div", {"class": "shipment"})] 

66 

67 def _parse_items(self): 

68 return [Item(x) for x in self.parsed.find_all("div", {"class": "yohtmlc-item"})] 

69 

70 def _parse_order_details_link(self): 

71 try: 

72 tag = self.parsed.find("a", {"class": "yohtmlc-order-details-link"}) 

73 if tag: 

74 return "{}{}".format(BASE_URL, tag.attrs["href"]) 

75 except AttributeError: 

76 logger.warning("When building Order, `order_details_link` could not be parsed.", exc_info=True) 

77 

78 def _parse_order_number(self): 

79 try: 

80 if self.order_details_link: 

81 parsed_url = urlparse(self.order_details_link) 

82 return parse_qs(parsed_url.query)["orderID"][0] 

83 else: 

84 tag = self.parsed.find("bdi", dir="ltr") 

85 return tag.text.strip() 

86 except (AttributeError, IndexError): 

87 # TODO: refactor entities to all extend a base entity, which has this base parse function with this except 

88 logger.warning("When building Order, `order_number` could not be parsed.", exc_info=True) 

89 

90 def _parse_grand_total(self): 

91 try: 

92 tag = self.parsed.find("div", {"class": "yohtmlc-order-total"}) 

93 if tag: 

94 tag = tag.find("span", {"class": "value"}) 

95 else: 

96 for tag in self.parsed.find("div", id="od-subtotals").find_all("div", {"class": "a-row"}): 

97 if "grand total" in tag.text.lower(): 

98 tag = tag.find("div", {"class": "a-span-last"}) 

99 return tag.text.strip().replace("$", "") 

100 except AttributeError: 

101 logger.warning("When building Order, `grand_total` could not be parsed.", exc_info=True) 

102 

103 def _parse_order_placed_date(self): 

104 try: 

105 tag = self.parsed.find("span", {"class": "order-date-invoice-item"}) 

106 if tag: 

107 date_str = tag.text.split("Ordered on")[1].strip() 

108 else: 

109 tag = self.parsed.find("div", {"class": "a-span3"}).find_all("span") 

110 date_str = tag[1].text.strip() 

111 return datetime.strptime(date_str, "%B %d, %Y").date() 

112 except (AttributeError, IndexError): 

113 logger.warning("When building Order, `order_placed_date` could not be parsed.", exc_info=True) 

114 

115 def _parse_recipient(self): 

116 try: 

117 tag = self.parsed.find("div", {"class": "displayAddressDiv"}) 

118 if not tag: 

119 script_id = self.parsed.find("div", 

120 id=lambda value: value and value.startswith("shipToInsertionNode")).attrs[ 

121 "id"] 

122 tag = self.parsed.find("script", 

123 id="shipToData-shippingAddress-{}".format(script_id.split("-")[2])) 

124 tag = BeautifulSoup(str(tag.contents[0]).strip(), "html.parser") 

125 return Recipient(tag) 

126 except (AttributeError, IndexError): 

127 logger.warning("When building Order, `recipient` could not be parsed.", exc_info=True) 

128 

129 def _parse_payment_method(self): 

130 try: 

131 tag = self.parsed.find("img", {"class": "pmts-payment-credit-card-instrument-logo"}) 

132 if tag: 

133 return tag.attrs["alt"] 

134 except (AttributeError, IndexError): 

135 logger.warning("When building Order, `payment_method` could not be parsed.", exc_info=True) 

136 

137 def _parse_payment_method_last_4(self): 

138 try: 

139 tag = self.parsed.find("img", {"class": "pmts-payment-credit-card-instrument-logo"}) 

140 if tag: 

141 ending_sibling = tag.find_next_siblings()[-1] 

142 return ending_sibling.text.split("ending in")[1].strip() 

143 except (AttributeError, IndexError): 

144 logger.warning("When building Order, `payment_method_last_4` could not be parsed.", exc_info=True) 

145 

146 def _parse_subtotal(self): 

147 try: 

148 for tag in self.parsed.find("div", id="od-subtotals").find_all("div", {"class": "a-row"}): 

149 if "subtotal" in tag.text.lower(): 

150 return tag.find("div", {"class": "a-span-last"}).text.strip().replace("$", "") 

151 except (AttributeError, IndexError): 

152 logger.warning("When building Order, `subtotal` could not be parsed.", exc_info=True) 

153 

154 def _parse_shipping_total(self): 

155 try: 

156 for tag in self.parsed.find("div", id="od-subtotals").find_all("div", {"class": "a-row"}): 

157 if "shipping" in tag.text.lower(): 

158 return tag.find("div", {"class": "a-span-last"}).text.strip().replace("$", "") 

159 except (AttributeError, IndexError): 

160 logger.warning("When building Order, `shipping_total` could not be parsed.", exc_info=True) 

161 

162 def _parse_subscription_discount(self): 

163 try: 

164 for tag in self.parsed.find("div", id="od-subtotals").find_all("div", {"class": "a-row"}): 

165 if "subscribe" in tag.text.lower(): 

166 return tag.find("div", {"class": "a-span-last"}).text.strip().replace("$", "") 

167 except (AttributeError, IndexError): 

168 logger.warning("When building Order, `subscription_discount` could not be parsed.", exc_info=True) 

169 

170 def _parse_total_before_tax(self): 

171 try: 

172 for tag in self.parsed.find("div", id="od-subtotals").find_all("div", {"class": "a-row"}): 

173 if "before tax" in tag.text.lower(): 

174 return tag.find("div", {"class": "a-span-last"}).text.strip().replace("$", "") 

175 except (AttributeError, IndexError): 

176 logger.warning("When building Order, `total_before_tax` could not be parsed.", exc_info=True) 

177 

178 def _parse_estimated_tax(self): 

179 try: 

180 for tag in self.parsed.find("div", id="od-subtotals").find_all("div", {"class": "a-row"}): 

181 if "estimated tax" in tag.text.lower(): 

182 return tag.find("div", {"class": "a-span-last"}).text.strip().replace("$", "") 

183 except (AttributeError, IndexError): 

184 logger.warning("When building Order, `estimated_tax` could not be parsed.", exc_info=True) 

185 

186 def _parse_refund_total(self): 

187 try: 

188 for tag in self.parsed.find("div", id="od-subtotals").find_all("div", {"class": "a-row"}): 

189 if "refund total" in tag.text.lower() and "tax refund" not in tag.text.lower(): 

190 return tag.find("div", {"class": "a-span-last"}).text.strip().replace("$", "") 

191 except (AttributeError, IndexError): 

192 logger.warning("When building Order, `refund_total` could not be parsed.", exc_info=True) 

193 

194 def _parse_order_shipping_date(self): 

195 try: 

196 # TODO: find a better way to do this 

197 if "Items shipped:" in self.parsed.text: 

198 date_str = self.parsed.text.split("Items shipped:")[1].strip().split("-")[0].strip() 

199 return datetime.strptime(date_str, "%B %d, %Y").date() 

200 except (AttributeError, IndexError): 

201 logger.warning("When building Order, `order_shipping_date` could not be parsed.", exc_info=True) 

202 

203 def _parse_refund_completed_date(self): 

204 try: 

205 # TODO: find a better way to do this 

206 if "Refund: Completed" in self.parsed.text: 

207 date_str = self.parsed.text.split("Refund: Completed")[1].strip().split("-")[0].strip() 

208 return datetime.strptime(date_str, "%B %d, %Y").date() 

209 except (AttributeError, IndexError): 

210 logger.warning("When building Order, `refund_completed_date` could not be parsed.", exc_info=True)