diff --git a/modules/vicsec/__init__.py b/modules/vicsec/__init__.py new file mode 100644 index 00000000..6e789a91 --- /dev/null +++ b/modules/vicsec/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 Oleg Plakhotniuk +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from .module import VicSecModule + +__all__ = ['VicSecModule'] diff --git a/modules/vicsec/browser.py b/modules/vicsec/browser.py new file mode 100644 index 00000000..a5204348 --- /dev/null +++ b/modules/vicsec/browser.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 Oleg Plakhotniuk +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from weboob.tools.capabilities.bank.transactions import \ + AmericanTransaction as AmTr +from weboob.browser import LoginBrowser, URL, need_login +from weboob.browser.pages import HTMLPage +from weboob.capabilities.base import Currency +from weboob.capabilities.shop import OrderNotFound, Order, Item, Payment +from weboob.exceptions import BrowserIncorrectPassword + +from datetime import datetime +from decimal import Decimal +from itertools import chain +import re + + +__all__ = ['VicSec'] + + +class VicSecPage(HTMLPage): + @property + def logged(self): + return bool(self.doc.xpath( + '//a[@href="https://www.victoriassecret.com/account/profile"]')) + + +class LoginPage(VicSecPage): + def login(self, email, password): + form = self.get_form(name='accountLogonForm') + form['j_username'] = email + form['j_password'] = password + form.submit() + + +class HistoryPage(VicSecPage): + def iter_orders(self): + return [onum for date, onum in sorted( + chain(self.orders(), self.returns()), reverse=True)] + + def orders(self): + for tr in self.doc.xpath('//table[@class="order-status"]/tbody[1]/tr'): + date = datetime.strptime(tr.xpath('td[1]/text()')[0], '%m/%d/%Y') + num = tr.xpath('td[2]/a/text()')[0] + status = tr.xpath('td[4]/span/text()')[0] + if status == u'Delivered': + yield date, num + + def returns(self): + for tr in self.doc.xpath('//table[@class="order-status"]/tbody[3]/tr'): + num = tr.xpath('td[1]/a/text()')[0] + date = datetime.strptime(tr.xpath('td[2]/text()')[0], '%m/%d/%Y') + status = tr.xpath('td[4]/span/text()')[0] + if status == u'Complete': + yield date, num + + +class OrderPage(VicSecPage): + def order(self): + order = Order(id=self.order_number()) + order.date = self.order_date() + order.tax = self.tax() + order.discount = self.discount() + order.shipping = self.shipping() + return order + + def payments(self): + for tr in self.doc.xpath('//tbody[@class="payment-summary"]' + '//th[text()="Payment Summary"]/../../../tbody/tr'): + method = tr.xpath('td[1]/text()')[0] + amount = tr.xpath('td[2]')[0].text_content().strip() + pmt = Payment() + pmt.date = self.order_date() + pmt.method = unicode(method) + pmt.amount = AmTr.decimal_amount(amount) + yield pmt + + def items(self): + for tr in self.doc.xpath('//tbody[@class="order-items"]/tr'): + label = tr.xpath('*//h1')[0].text_content() + price = AmTr.decimal_amount(re.match(r'^\s*([^\s]+)(\s+.*)?', + tr.xpath('*//div[@class="price"]')[0].text_content(), + re.DOTALL).group(1)) + url = 'http:' + tr.xpath('*//img/@src')[0] + item = Item() + item.label = unicode(label) + item.url = unicode(url) + item.price = price + yield item + + def is_void(self): + return not self.doc.xpath('//tbody[@class="order-items"]/tr') + + def order_number(self): + return self.order_info(u'Order Number') + + def order_date(self): + return datetime.strptime(self.order_info(u'Order Date'), '%m/%d/%Y') + + def tax(self): + return self.payment_part(u'Sales Tax') + + def shipping(self): + return self.payment_part(u'Shipping & Handling') + + def order_info(self, which): + info = self.doc.xpath('//p[@class="orderinfo details"]' + )[0].text_content() + return re.match(u'.*%s:\\s+([^\\s]+)\\s'%which,info,re.DOTALL).group(1) + + def discount(self): + # Sometimes subtotal doesn't add up with items. + # I saw that "Special Offer" was actually added to the item's price, + # instead of being subtracted. Looks like a bug on VS' side. + # To compensate for it I'm correcting discount value. + dcnt = self.payment_part(u'Special Offer') + subt = self.payment_part(u'Merchandise Subtotal') + rett = self.payment_part(u'Return Merchandise Total') + items = sum(i.price for i in self.items()) + return dcnt + subt + rett - items + + def payment_part(self, which): + for node in self.doc.xpath('//tbody[@class="payment-summary"]' + '//td[contains(text(),"%s")]/../td[2]' % which): + x = node.text_content().strip() + return Decimal(0) if x == u'FREE' else AmTr.decimal_amount(x) + return Decimal(0) + + +class VicSec(LoginBrowser): + BASEURL = 'https://www.victoriassecret.com' + login = URL(r'/account/signin/overlay$', LoginPage) + history = URL(r'/account/orderhistory$', HistoryPage) + order = URL(r'/account/orderdetails\?orderNumber=(?P\d+)$', + r'/account/orderdetails.*$', + OrderPage) + unknown = URL(r'/.*$', VicSecPage) + + def get_currency(self): + # Victoria's Secret uses only U.S. dollars. + return Currency.get_currency(u'$') + + def get_order(self, id_): + return self.to_order(id_).order() + + def iter_orders(self): + for order in self.to_history().iter_orders(): + yield self.to_order(order).order() + + def iter_payments(self, order): + return self.to_order(order.id).payments() + + def iter_items(self, order): + return self.to_order(order.id).items() + + @need_login + def to_history(self): + self.history.stay_or_go() + assert self.history.is_here() + return self.page + + @need_login + def to_order(self, order_num): + self.order.stay_or_go(order_num=order_num) + assert self.order.is_here(order_num=order_num) + if self.page.is_void(): + raise OrderNotFound() + return self.page + + def do_login(self): + self.session.cookies.clear() + # Need to go there two times. Perhaps because of cookies... + self.login.go() + self.login.go().login(self.username, self.password) + self.history.go() + if not self.history.is_here(): + raise BrowserIncorrectPassword() diff --git a/modules/vicsec/favicon.png b/modules/vicsec/favicon.png new file mode 100644 index 00000000..5b653f60 Binary files /dev/null and b/modules/vicsec/favicon.png differ diff --git a/modules/vicsec/module.py b/modules/vicsec/module.py new file mode 100644 index 00000000..7d51d1db --- /dev/null +++ b/modules/vicsec/module.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 Oleg Plakhotniuk +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from weboob.capabilities.shop import CapShop +from weboob.tools.backend import Module, BackendConfig +from weboob.tools.value import ValueBackendPassword + +from .browser import VicSec + + +__all__ = ['VicSecModule'] + + +class VicSecModule(Module, CapShop): + NAME = 'vicsec' + MAINTAINER = u'Oleg Plakhotniuk' + EMAIL = 'olegus8@gmail.com' + VERSION = '1.1' + LICENSE = 'AGPLv3+' + DESCRIPTION = u'Victoria\'s Secret' + CONFIG = BackendConfig( + ValueBackendPassword('email', label='Username', masked=False), + ValueBackendPassword('password', label='Password')) + BROWSER = VicSec + + def create_default_browser(self): + return self.create_browser(self.config['email'].get(), + self.config['password'].get()) + + def get_currency(self): + return self.browser.get_currency() + + def get_order(self, id_): + return self.browser.get_order(id_) + + def iter_orders(self): + return self.browser.iter_orders() + + def iter_payments(self, order): + return self.browser.iter_payments(order) + + def iter_items(self, order): + return self.browser.iter_items(order) diff --git a/modules/vicsec/test.py b/modules/vicsec/test.py new file mode 100644 index 00000000..ab5ca00c --- /dev/null +++ b/modules/vicsec/test.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 Oleg Plakhotniuk +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + +from weboob.tools.test import BackendTest + + +class VicSecTest(BackendTest): + MODULE = 'vicsec' + + def test_history(self): + """ + Test that at least one item was ordered in the whole history. + """ + b = self.backend + items = (i for o in b.iter_orders() for i in b.iter_items(o)) + item = next(items, None) + self.assertNotEqual(item, None)