Coverage for amazonorders/entity/order.py: 94.53%
128 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-16 23:55 +0000
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-16 23:55 +0000
1import logging
2from datetime import datetime, date
3from typing import List, Optional, TypeVar
4from urllib.parse import parse_qs
5from urllib.parse import urlparse
7from bs4 import BeautifulSoup, Tag
9from amazonorders.entity.item import Item
10from amazonorders.entity.parsable import Parsable
11from amazonorders.entity.recipient import Recipient
12from amazonorders.entity.shipment import Shipment
13from amazonorders.session import BASE_URL
15__author__ = "Alex Laird"
16__copyright__ = "Copyright 2024, Alex Laird"
17__version__ = "0.0.7"
19logger = logging.getLogger(__name__)
21Entity = TypeVar('Entity', bound='Order')
24class Order(Parsable):
25 def __init__(self,
26 parsed: Tag,
27 full_details: bool = False,
28 clone: Optional[Entity] = None) -> None:
29 super().__init__(parsed)
31 self.full_details: bool = full_details
33 self.shipments: List[Shipment] = clone.shipments if clone else self._parse_shipments()
34 self.items: List[Item] = clone.items if clone else self._parse_items()
35 self.order_details_link: Optional[str] = clone.order_details_link if clone else self.safe_parse(
36 self._parse_order_details_link)
37 self.order_number: str = clone.order_number if clone else self.safe_parse(self._parse_order_number)
38 self.grand_total: float = clone.grand_total if clone else self.safe_parse(self._parse_grand_total)
39 self.order_placed_date: date = clone.order_placed_date if clone else self.safe_parse(
40 self._parse_order_placed_date)
41 self.recipient: Recipient = clone.recipient if clone else self.safe_parse(self._parse_recipient)
43 if self.full_details:
44 self.items: List[Item] = self._parse_items()
45 self.payment_method: Optional[str] = self._parse_payment_method()
46 self.payment_method_last_4: Optional[str] = self._parse_payment_method_last_4()
47 self.subtotal: Optional[float] = self._parse_subtotal()
48 self.shipping_total: Optional[float] = self._parse_shipping_total()
49 self.subscription_discount: Optional[float] = self._parse_subscription_discount()
50 self.total_before_tax: Optional[float] = self._parse_total_before_tax()
51 self.estimated_tax: Optional[float] = self._parse_estimated_tax()
52 self.refund_total: Optional[float] = self._parse_refund_total()
53 self.order_shipped_date: Optional[date] = self._parse_order_shipping_date()
54 self.refund_completed_date: Optional[date] = self._parse_refund_completed_date()
56 def __repr__(self) -> str:
57 return "<Order #{}: \"{}\">".format(self.order_number, self.items)
59 def __str__(self) -> str: # pragma: no cover
60 return "Order #{}: \"{}\"".format(self.order_number, self.items)
62 def _parse_shipments(self) -> List[Shipment]:
63 return [Shipment(x) for x in self.parsed.find_all("div", {"class": "shipment"})]
65 def _parse_items(self) -> List[Item]:
66 return [Item(x) for x in self.parsed.find_all("div", {"class": "yohtmlc-item"})]
68 def _parse_order_details_link(self) -> Optional[str]:
69 tag = self.parsed.find("a", {"class": "yohtmlc-order-details-link"})
70 if tag:
71 return "{}{}".format(BASE_URL, tag.attrs["href"])
72 else:
73 return None
75 def _parse_order_number(self) -> str:
76 if self.order_details_link:
77 parsed_url = urlparse(self.order_details_link)
78 return parse_qs(parsed_url.query)["orderID"][0]
79 else:
80 tag = self.parsed.find("bdi", dir="ltr")
81 return tag.text.strip()
83 def _parse_grand_total(self) -> float:
84 tag = self.parsed.find("div", {"class": "yohtmlc-order-total"})
85 if tag:
86 tag = tag.find("span", {"class": "value"})
87 else:
88 for tag in self.parsed.find("div", id="od-subtotals").find_all("div", {"class": "a-row"}):
89 if "grand total" in tag.text.lower():
90 tag = tag.find("div", {"class": "a-span-last"})
91 break
92 return float(tag.text.strip().replace("$", ""))
94 def _parse_order_placed_date(self) -> date:
95 tag = self.parsed.find("span", {"class": "order-date-invoice-item"})
96 if tag:
97 date_str = tag.text.split("Ordered on")[1].strip()
98 else:
99 tag = self.parsed.find("div", {"class": "a-span3"}).find_all("span")
100 date_str = tag[1].text.strip()
101 return datetime.strptime(date_str, "%B %d, %Y").date()
103 def _parse_recipient(self) -> Recipient:
104 tag = self.parsed.find("div", {"class": "displayAddressDiv"})
105 if not tag:
106 script_id = self.parsed.find("div",
107 id=lambda value: value and value.startswith("shipToInsertionNode")).attrs[
108 "id"]
109 tag = self.parsed.find("script",
110 id="shipToData-shippingAddress-{}".format(script_id.split("-")[2]))
111 tag = BeautifulSoup(str(tag.contents[0]).strip(), "html.parser")
112 return Recipient(tag)
114 def _parse_payment_method(self) -> Optional[str]:
115 tag = self.parsed.find("img", {"class": "pmts-payment-credit-card-instrument-logo"})
116 if tag:
117 return tag.attrs["alt"]
118 else:
119 return None
121 def _parse_payment_method_last_4(self) -> Optional[str]:
122 tag = self.parsed.find("img", {"class": "pmts-payment-credit-card-instrument-logo"})
123 if tag:
124 ending_sibling = tag.find_next_siblings()[-1]
125 return ending_sibling.text.split("ending in")[1].strip()
126 else:
127 return None
129 def _parse_subtotal(self) -> Optional[float]:
130 for tag in self.parsed.find("div", id="od-subtotals").find_all("div", {"class": "a-row"}):
131 if "subtotal" in tag.text.lower():
132 return float(tag.find("div", {"class": "a-span-last"}).text.strip().replace("$", ""))
134 return None
136 def _parse_shipping_total(self) -> Optional[float]:
137 for tag in self.parsed.find("div", id="od-subtotals").find_all("div", {"class": "a-row"}):
138 if "shipping" in tag.text.lower():
139 return float(tag.find("div", {"class": "a-span-last"}).text.strip().replace("$", ""))
141 return None
143 def _parse_subscription_discount(self) -> Optional[float]:
144 for tag in self.parsed.find("div", id="od-subtotals").find_all("div", {"class": "a-row"}):
145 if "subscribe" in tag.text.lower():
146 return float(tag.find("div", {"class": "a-span-last"}).text.strip().replace("$", ""))
148 return None
150 def _parse_total_before_tax(self) -> Optional[float]:
151 for tag in self.parsed.find("div", id="od-subtotals").find_all("div", {"class": "a-row"}):
152 if "before tax" in tag.text.lower():
153 return float(tag.find("div", {"class": "a-span-last"}).text.strip().replace("$", ""))
155 return None
157 def _parse_estimated_tax(self) -> Optional[float]:
158 for tag in self.parsed.find("div", id="od-subtotals").find_all("div", {"class": "a-row"}):
159 if "estimated tax" in tag.text.lower():
160 return float(tag.find("div", {"class": "a-span-last"}).text.strip().replace("$", ""))
162 return None
164 def _parse_refund_total(self) -> Optional[float]:
165 for tag in self.parsed.find("div", id="od-subtotals").find_all("div", {"class": "a-row"}):
166 if "refund total" in tag.text.lower() and "tax refund" not in tag.text.lower():
167 return float(tag.find("div", {"class": "a-span-last"}).text.strip().replace("$", ""))
169 return None
171 def _parse_order_shipping_date(self) -> Optional[date]:
172 # TODO: find a better way to do this
173 if "Items shipped:" in self.parsed.text:
174 date_str = self.parsed.text.split("Items shipped:")[1].strip().split("-")[0].strip()
175 return datetime.strptime(date_str, "%B %d, %Y").date()
176 else:
177 return None
179 def _parse_refund_completed_date(self) -> Optional[date]:
180 # TODO: find a better way to do this
181 if "Refund: Completed" in self.parsed.text:
182 date_str = self.parsed.text.split("Refund: Completed")[1].strip().split("-")[0].strip()
183 return datetime.strptime(date_str, "%B %d, %Y").date()
184 else:
185 return None