dominosjp 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1a8eff1dcad6edc6236af5a90239ecd2afd0cdc3
4
+ data.tar.gz: 3ca1d4888bb591523b1dbe3921e71ff6c3ec811f
5
+ SHA512:
6
+ metadata.gz: 8c63d14703893d31846c4d2691b8c73ea49b5cda9ca69fa937cb40beff10e99a42820d562c8d0f7c50d15a47ff2e76f7796b73a9741f557e3c861dd913596793
7
+ data.tar.gz: 53dae35002494984757b89e65dbfda652ed6dfb26e0f9132741ee6948560076b33d5e7e8c84c297b80ec557e8c391678274b3ba307b09604984ea41e95e9149b
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Mahdi Bchetnia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # DominosJP πŸ•πŸ›΅πŸ‡―πŸ‡΅
2
+ πŸ•Domino's Pizza Japan CLI πŸ•
3
+
4
+ ![](https://i.imgur.com/CRaTrSE.jpg)
5
+
6
+ ### Requirements
7
+
8
+ - A [Domino's Pizza Japan](https://www.dominos.jp/eng/) account that you've ordered with at least once in the past
9
+
10
+ ### Install
11
+
12
+ ```bash
13
+ $ gem install dominosjp
14
+ ```
15
+
16
+ ### Usage
17
+
18
+ ```bash
19
+ $ dominosjp
20
+ ```
21
+ and follow the instructions
22
+
23
+ ### Features
24
+
25
+ - Order a pizza in less than a minute
26
+ - Save your preferences for even faster ordering
27
+ - Finds the best coupon for your order from your coupon box, by calculating the real value
28
+
29
+ ### Preferences
30
+
31
+ Not really pizza-related, but by providing default values in a `.dominosjp.yml` file in your home directory you can skip most steps. See [.dominosjp.yml](blob/master/.dominosjp.yml) for examples.
32
+
33
+ Note: All keys are optional. It even allows for partial credit card info.
34
+
35
+ ### Limitations/TODO
36
+
37
+ (Prefixed by a difficulty estimation from 1 to 5)
38
+
39
+ - (1) Allow for paying via cash (credit card-only now)
40
+ - (5) Allow for selecting pizza toppings
41
+ - (4) Allow for selecting pizza size/cut type/number of slices
42
+ - (2) Allow for selecting sides (not only pizzas), (5) maybe even the special menu
43
+ - (5) Extra: Pizza Tracking via the CLI, and hopefully automate the Mystery Deal
44
+
45
+ ### Contact
46
+
47
+ [@inket](https://github.com/inket) / [@inket](https://twitter.com/inket) on Twitter / [mahdi.jp](https://mahdi.jp)
data/bin/dominosjp ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/dominosjp"
5
+
6
+ begin
7
+ dominos = DominosJP.new
8
+ dominos.login
9
+ dominos.order
10
+ rescue Interrupt
11
+ puts "Stopped by user"
12
+ end
data/lib/dominosjp.rb ADDED
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+ require "colorize"
3
+ require "highline"
4
+ require "inquirer"
5
+ require "nokogiri"
6
+
7
+ require_relative "request"
8
+ require_relative "preferences"
9
+ require_relative "order_address"
10
+ require_relative "order_coupon"
11
+ require_relative "order_information"
12
+ require_relative "order_payment"
13
+ require_relative "order_review"
14
+ require_relative "pizza"
15
+ require_relative "pizza_selector"
16
+
17
+ class DominosJP
18
+ attr_accessor :order_address, :order_information
19
+ attr_accessor :order_review
20
+ attr_accessor :order_coupon, :order_payment
21
+
22
+ def initialize(login_details = {})
23
+ @email = login_details[:email] || Preferences.instance.email
24
+ @password = login_details[:password]
25
+
26
+ self.order_address = OrderAddress.new
27
+ self.order_information = OrderInformation.new
28
+ self.order_review = OrderReview.new
29
+ self.order_coupon = OrderCoupon.new
30
+ self.order_payment = OrderPayment.new
31
+ end
32
+
33
+ def login
34
+ @email ||= Ask.input("Email")
35
+ @password ||= HighLine.new.ask("Password: ") { |q| q.echo = "*" }
36
+
37
+ Request.post(
38
+ "https://order.dominos.jp/eng/login/login/",
39
+ { "emailAccount" => @email, "webPwd" => @password },
40
+ expect: :redirect, failure: "Couldn't log in successfully"
41
+ )
42
+ end
43
+
44
+ def order
45
+ order_address.input
46
+ order_address.validate
47
+
48
+ order_information.input
49
+ order_information.validate
50
+ order_information.display
51
+ order_information.confirm
52
+
53
+ PizzaSelector.select_pizzas
54
+ # TODO: allow selecting sides
55
+
56
+ order_review.display
57
+
58
+ order_coupon.total_price_without_tax = order_review.total_price_without_tax
59
+ order_coupon.input
60
+ order_coupon.validate
61
+
62
+ order_review.display
63
+
64
+ order_payment.default_name = order_information.name
65
+ order_payment.input
66
+ order_payment.validate
67
+ order_payment.display
68
+
69
+ order_review.page = order_payment.page
70
+ order_review.display
71
+
72
+ order_payment.confirm
73
+ end
74
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+ class OrderAddress
3
+ attr_accessor :address
4
+
5
+ def input
6
+ response = Request.get("https://order.dominos.jp/eng/receipt/",
7
+ expect: :ok, failure: "Couldn't get order types page")
8
+
9
+ addresses = Addresses.from(response.body)
10
+ index = Ask.list "Choose an address", addresses.selection_list
11
+ self.address = addresses[index]
12
+ end
13
+
14
+ def validate
15
+ raise "Missing attributes" unless address
16
+
17
+ # Get the default parameters and add in the delivery address
18
+ params = default_params.merge("todokeSeq" => address.id)
19
+
20
+ Request.post("https://order.dominos.jp/eng/receipt/setReceipt", params,
21
+ expect: :redirect, to: "https://order.dominos.jp/eng/receipt/input/",
22
+ failure: "Couldn't set the delivery address")
23
+ end
24
+
25
+ private
26
+
27
+ def default_params
28
+ {
29
+ # Receipt method: 1=delivery, 3=pickup
30
+ "receiptMethod" => "1",
31
+ # Rest is untouched
32
+ "tenpoC" => "",
33
+ "jushoC" => "",
34
+ "kokyakuJushoBanchi" => "",
35
+ "banchiCheckBox" => "",
36
+ "buildNm" => "",
37
+ "buildCheckBox" => "",
38
+ "todokeShortNm" => "",
39
+ "kigyoNm" => "",
40
+ "bushoNm" => "",
41
+ "naisen" => "",
42
+ "targetYmd" => nil,
43
+ "targetYmdhm" => nil,
44
+ "gpsPinpointF" => false
45
+ }
46
+ end
47
+ end
48
+
49
+ class Addresses < Array
50
+ def self.from(source)
51
+ doc = Nokogiri::HTML(source)
52
+
53
+ Addresses.new(
54
+ doc.css(".l-section.m-addressSelect .addressSelect_content").map { |el| Address.new(el) }
55
+ )
56
+ end
57
+
58
+ def selection_list
59
+ map(&:list_item)
60
+ end
61
+ end
62
+
63
+ class Address
64
+ attr_accessor :id, :label, :address, :estimation
65
+
66
+ def initialize(element)
67
+ self.label = element.css(".addressSelect_labelName").text.strip
68
+ self.address = element.css(".addressSelect_information_address").text.strip
69
+ self.estimation = element.css(".time_content_text").text.strip
70
+ self.id = element.css("input[name=todokeSeq]").first["value"].strip
71
+ end
72
+
73
+ def list_item
74
+ [label, address, estimation].join("\n ")
75
+ end
76
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+ class OrderCoupon
3
+ attr_accessor :total_price_without_tax
4
+ attr_accessor :add_coupon
5
+ attr_accessor :coupon
6
+
7
+ def input
8
+ self.add_coupon = Ask.confirm "Add a coupon?"
9
+ return unless add_coupon
10
+
11
+ response = Request.get("https://order.dominos.jp/eng/coupon/use/",
12
+ expect: :ok, failure: "Couldn't get coupons list")
13
+
14
+ coupons = Coupons.from(response.body, total_price_without_tax)
15
+ selected_coupon_index = Ask.list "Choose a coupon", coupons.selection_list
16
+ self.coupon = coupons[selected_coupon_index]
17
+ end
18
+
19
+ def validate
20
+ return unless add_coupon
21
+
22
+ unless coupon.usable?
23
+ puts "This coupon cannot be used."
24
+ return
25
+ end
26
+
27
+ Request.post("https://order.dominos.jp/eng/webapi/sides/setUserCoupon/", coupon.params,
28
+ expect: :ok, failure: "Couldn't add coupon")
29
+ end
30
+ end
31
+
32
+ class Coupons < Array
33
+ def self.from(source, total_price_without_tax)
34
+ doc = Nokogiri::HTML(source)
35
+ coupons = doc.css("li").map { |item| Coupon.new(item, total_price_without_tax) }
36
+
37
+ # Sort coupons by real value, expiration date while deranking those that cannot be used (error)
38
+ coupons = [coupons.reject(&:error), coupons.select(&:error)].map do |coups|
39
+ coups.group_by(&:real_value).sort.reverse.map do |_real_value, same_value_coupons|
40
+ same_value_coupons.sort_by(&:expiry)
41
+ end
42
+ end.flatten
43
+
44
+ Coupons.new(coupons)
45
+ end
46
+
47
+ def selection_list
48
+ map(&:list_item)
49
+ end
50
+ end
51
+
52
+ class Coupon
53
+ attr_accessor :name, :expiry, :error, :couponcd, :couponseq, :expires_soon, :real_value
54
+
55
+ def initialize(item, total_price_without_tax)
56
+ name_element_text = item.css("h4").text
57
+ coupon_name = name_element_text.sub("\\", "Β₯").sub("Expires soon", "")
58
+ expires_soon = name_element_text.include?("Expires soon") ? "Expires soon" : ""
59
+
60
+ coupon_link = item.css(".jso-userCuponUse").first || {}
61
+
62
+ yen_value = coupon_name.scan(/Β₯(\d+)/).flatten.first.to_i
63
+ percent_value = coupon_name.scan(/(\d+)%/).flatten.first.to_i
64
+
65
+ if yen_value != 0
66
+ real_value = yen_value * 1.08 # 8% tax
67
+ elsif percent_value != 0
68
+ real_value = (total_price_without_tax / (100 / percent_value)) * 1.08 # 8% tax
69
+ end
70
+
71
+ error = item.css(".m-input__error").text
72
+ error = error && error.strip != "" ? "\n #{error}" : nil
73
+
74
+ self.name = coupon_name
75
+ self.expiry = item.css(".m-entryPeriod").text.scan(/\d{4}-\d{2}-\d{2}/).first
76
+ self.error = error
77
+ self.couponcd = coupon_link["couponcd"]
78
+ self.couponseq = coupon_link["couponseq"]
79
+ self.expires_soon = expires_soon
80
+ self.real_value = real_value.to_i
81
+ end
82
+
83
+ def usable?
84
+ couponcd && couponseq
85
+ end
86
+
87
+ def params
88
+ { couponcd: couponcd, couponseq: couponseq }.compact
89
+ end
90
+
91
+ def list_item
92
+ "#{name.colorize(:blue)} (-Β₯#{real_value.to_s.colorize(:green)}) "\
93
+ "#{expires_soon.colorize(:yellow)} #{expiry} #{error.to_s.colorize(:red)}".strip
94
+ end
95
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+ class OrderInformation
3
+ attr_accessor :name, :phone_number
4
+
5
+ def input
6
+ response = Request.get("https://order.dominos.jp/eng/receipt/input/",
7
+ expect: :ok, failure: "Couldn't get information input page")
8
+
9
+ saved_name = Name.from(response.body)
10
+ phone_numbers = PhoneNumbers.from(response.body)
11
+
12
+ self.name = Preferences.instance.name || Ask.input("Name", default: saved_name)
13
+
14
+ self.phone_number = phone_numbers.find_number(Preferences.instance.phone_number)
15
+ unless phone_number
16
+ phone_number_index = Ask.list "Phone Number", phone_numbers.selection_list
17
+ self.phone_number = phone_numbers[phone_number_index]
18
+ end
19
+
20
+ @first_response = response
21
+ end
22
+
23
+ def validate
24
+ raise "Missing attributes" unless name && phone_number
25
+
26
+ # Get the default parameters and add in the client name and phone number
27
+ params = default_params.merge(
28
+ "kokyakuNm" => name,
29
+ "telSeq" => phone_number.value
30
+ )
31
+
32
+ @second_response = Request.post("https://order.dominos.jp/eng/receipt/confirm", params,
33
+ expect: :ok, failure: "Couldn't set your information") do |resp|
34
+ resp.body.include?("Order Type, Day&Time and Your Store")
35
+ end
36
+ end
37
+
38
+ def display
39
+ doc = Nokogiri::HTML(@second_response.body)
40
+ info = doc.css(".m-input__heading").map(&:text).
41
+ zip(doc.css(".section_content_table_td").map(&:text)).to_h
42
+ @page_params = doc.css("input[type=hidden]").map do |input|
43
+ [input["name"], input["value"]]
44
+ end.to_h
45
+
46
+ info.each { |title, value| puts "#{title.colorize(:blue)}: #{value}" }
47
+ end
48
+
49
+ def confirm
50
+ raise "Stopped by user" unless Ask.confirm "Continue?"
51
+
52
+ Request.post("https://order.dominos.jp/eng/receipt/complete", @page_params,
53
+ expect: :redirect, to: "https://order.dominos.jp/eng/menu/",
54
+ failure: "Couldn't validate your information")
55
+ end
56
+
57
+ private
58
+
59
+ def default_params
60
+ kokyaku_input = Nokogiri::HTML(@first_response.body).css("input[name=kokyakuC]").first
61
+ raise "Couldn't find client information" unless kokyaku_input
62
+
63
+ {
64
+ "kokyakuC" => kokyaku_input["value"],
65
+ # Rest is untouched
66
+ "errorMessage" => "",
67
+ "receiptMethod" => "1", # Receipt method again...
68
+ "deleteTelSeq" => "",
69
+ "telNoRadio" => "0",
70
+ "telNo" => ""
71
+ }
72
+ end
73
+ end
74
+
75
+ class Name
76
+ def self.from(source)
77
+ doc = Nokogiri::HTML(source)
78
+ input = doc.css("input[name=kokyakuNm]").first
79
+ raise "Couldn't get name field from information input page" unless input
80
+
81
+ input["value"]
82
+ end
83
+ end
84
+
85
+ class PhoneNumbers < Array
86
+ def self.from(source)
87
+ doc = Nokogiri::HTML(source)
88
+ numbers = doc.css("select[name=telSeq] > option").map { |option| PhoneNumber.new(option) }
89
+
90
+ if numbers.empty?
91
+ raise "Couldn't find any saved phone numbers in the information input page"
92
+ end
93
+
94
+ PhoneNumbers.new(numbers)
95
+ end
96
+
97
+ def find_number(number)
98
+ detect { |phone_number| phone_number.number == number }
99
+ end
100
+
101
+ def selection_list
102
+ map(&:list_item)
103
+ end
104
+ end
105
+
106
+ class PhoneNumber
107
+ attr_accessor :number, :value
108
+
109
+ def initialize(option)
110
+ self.number = option.text
111
+ self.value = option["value"]
112
+ end
113
+
114
+ def list_item
115
+ number
116
+ end
117
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+ require "credit_card_validations"
3
+ require "credit_card_validations/string"
4
+
5
+ class OrderPayment
6
+ attr_accessor :default_name
7
+ attr_accessor :note
8
+ attr_accessor :last_review
9
+ attr_accessor :page
10
+
11
+ def input
12
+ Request.get("https://order.dominos.jp/eng/regi/",
13
+ expect: :ok, failure: "Couldn't get payment page")
14
+
15
+ puts
16
+ puts
17
+ puts "#{"Payment information".colorize(:blue)} (you will be able to review your order later)"
18
+
19
+ @credit_card = Preferences.instance.credit_card || CreditCard.new
20
+ @credit_card.input(default_name)
21
+
22
+ self.note = Preferences.instance.note ||
23
+ Ask.input("Any special requests? (not food preparation requests)")
24
+ end
25
+
26
+ def validate
27
+ params = default_params.merge("bikoText" => note).merge(@credit_card.params)
28
+ response = Request.post("https://order.dominos.jp/eng/regi/confirm", params,
29
+ expect: :ok, failure: "Couldn't submit payment information")
30
+ doc = Nokogiri::HTML(response.body)
31
+
32
+ token_input = doc.css("input[name='org.apache.struts.taglib.html.TOKEN']").first
33
+ raise "Couldn't get token for order validation" unless token_input && token_input["value"]
34
+
35
+ @insert_params = doc.css("input").map { |input| [input["name"], input["value"]] }.to_h
36
+
37
+ self.last_review = OrderLastReview.new(doc)
38
+ self.page = response.body
39
+ end
40
+
41
+ def display
42
+ puts last_review
43
+ end
44
+
45
+ def confirm
46
+ puts
47
+
48
+ unless Ask.confirm "Place order?"
49
+ puts "Stopped by user"
50
+ return
51
+ end
52
+
53
+ Request.post("https://order.dominos.jp/eng/regi/insert", @insert_params,
54
+ expect: :redirect, to: %r{\Ahttps://order\.dominos\.jp/eng/regi/complete/\?},
55
+ failure: "Order couldn't be placed for some reason :(")
56
+
57
+ puts
58
+ puts "Success!"
59
+ puts "Be sure to check the Domino's Pizza website in your browser "\
60
+ "to track your order status via the Pizza Tracker, and win a Mystery Deal coupon"
61
+ end
62
+
63
+ private
64
+
65
+ def default_params
66
+ {
67
+ "inquiryRiyoDStr" => "undefined",
68
+ "inquiryCardComCd" => "undefined",
69
+ "inquiryCardBrand" => "undefined",
70
+ "inquiryCreditCardNoXXX" => "undefined",
71
+ "inquiryGoodThruMonth" => "undefined",
72
+ "inquiryGoodThruYear" => "undefined",
73
+ "creditCard" => "undefined",
74
+ "receiptK" => "0",
75
+ "exteriorPayment" => "4",
76
+ "reuseCreditDiv" => "1",
77
+ "rakutenPayment" => "1",
78
+ "isDisplayMailmagaArea" => "true",
79
+ "isProvisionalKokyakuModalView" => "false",
80
+ "isProvisionalKokyaku" => "false"
81
+ }
82
+ end
83
+ end
84
+
85
+ class OrderLastReview
86
+ attr_accessor :doc
87
+
88
+ def initialize(doc)
89
+ self.doc = doc
90
+ end
91
+
92
+ def to_s
93
+ sections = doc.css(".l-section").map do |section|
94
+ next unless section.css(".m-heading__caption").count.positive?
95
+
96
+ section_name = section.css(".m-heading__caption").text.strip.gsub(/\s+/, " ").colorize(:green)
97
+ rows = section.css("tr").map do |row|
98
+ th = row.css(".m-input__heading").first || row.css("th").first
99
+ th_text = th.text.strip.gsub(/\s+/, " ").colorize(:blue)
100
+ td_text = row.css(".section_content_table_td").text.
101
+ gsub(/ +/, " ").gsub(/\t+/, "").gsub(/(?:\r\n)+/, "\r\n").strip
102
+
103
+ "#{th_text}\n#{td_text}"
104
+ end
105
+
106
+ "\n#{section_name}\n#{rows.join("\n")}"
107
+ end
108
+
109
+ sections.join("\n")
110
+ end
111
+ end
112
+
113
+ class CreditCard
114
+ attr_accessor :number, :cvv
115
+ attr_accessor :expiration_date
116
+ attr_accessor :name_on_card
117
+
118
+ VALUES = {
119
+ visa: "00200",
120
+ mastercard: "00300",
121
+ jcb: "00500",
122
+ amex: "00400",
123
+ diners: "00100",
124
+ nicos: "00600"
125
+ }.freeze
126
+
127
+ def initialize(config = {})
128
+ info = config.map { |key, value| [(key.to_sym rescue key), value.to_s] }.to_h
129
+
130
+ self.number = info[:number] || ""
131
+ self.cvv = info[:cvv]
132
+ self.expiration_date = info[:expiration_date]
133
+ self.name_on_card = info[:name]
134
+ end
135
+
136
+ def input(default_name = nil)
137
+ loop do
138
+ until number.valid_credit_card_brand?(:visa, :mastercard, :jcb, :amex, :diners)
139
+ puts "Invalid card number" unless number == ""
140
+ self.number = Ask.input "Credit Card Number"
141
+ end
142
+
143
+ unless number.credit_card_brand == :diners
144
+ self.cvv ||= HighLine.new.ask("CVV: ") { |q| q.echo = "*" }
145
+ end
146
+
147
+ expiration_month, expiration_year = (expiration_date || "").split("/")
148
+ while !(1..12).cover?(expiration_month.to_i) || !(17..31).cover?(expiration_year.to_i)
149
+ self.expiration_date = Ask.input "Expiration Date (mm/yy)"
150
+ expiration_month, expiration_year = expiration_date.split("/")
151
+ end
152
+
153
+ self.name_on_card ||= Ask.input "Name on Card", default: default_name
154
+
155
+ break if valid?
156
+ end
157
+ end
158
+
159
+ def params
160
+ {
161
+ "existCreditCardF" => "",
162
+ "reuseCredit_check" => "1", # Seems to be 1 but it doesn't save the CC info
163
+ "cardComCd" => VALUES[number.credit_card_brand],
164
+ "creditCardNo" => number,
165
+ "creditCardSecurityCode" => cvv,
166
+ "creditCardSignature" => name_on_card,
167
+ "goodThruMonth" => expiration_date.split("/").first.rjust(2, "0"),
168
+ "goodThruYear" => expiration_date.split("/").last
169
+ }
170
+ end
171
+
172
+ def valid?
173
+ response = Request.post(
174
+ "https://order.dominos.jp/eng/webapi/regi/validate/creditCard/", params,
175
+ expect: :ok, failure: "Couldn't validate credit card info"
176
+ )
177
+
178
+ result = JSON.parse(response.body)
179
+ if result["errorDetails"]
180
+ puts result
181
+ return false
182
+ end
183
+
184
+ true
185
+ end
186
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+ class OrderReview
3
+ attr_accessor :page
4
+ attr_accessor :total_price, :total_price_without_tax
5
+
6
+ def display
7
+ puts
8
+ puts
9
+ puts "Review Your Order".colorize(:red)
10
+
11
+ source = page || default_page
12
+
13
+ # Order items
14
+ puts OrderItems.from(source)
15
+ puts CouponItems.from(source)
16
+
17
+ # General information
18
+ puts retrieve_prices(source)
19
+ end
20
+
21
+ private
22
+
23
+ def retrieve_prices(source)
24
+ doc = Nokogiri::HTML(source)
25
+ total_price_element = doc.css(".totalPrice_taxin")
26
+ total_price_title = total_price_element.css("dt").text.strip.gsub(/\s+/, " ")
27
+ total_price_string = total_price_element.css("dd").text.strip.gsub(/\s+/, " ")
28
+
29
+ self.total_price = total_price_string.delete(",").scan(/Β₯(\d+)/).flatten.first.to_i
30
+ self.total_price_without_tax = total_price / 108 * 100 # 8% tax
31
+
32
+ "\n#{total_price_title}: #{total_price_string.colorize(:red)}\n"\
33
+ "#{doc.css(".totalPrice_tax").text}"
34
+ end
35
+
36
+ def default_page
37
+ Request.get("https://order.dominos.jp/eng/pizza/search/",
38
+ expect: :ok, failure: "Couldn't get pizza list page").body
39
+ end
40
+ end
41
+
42
+ class OrderItems < Array
43
+ def self.from(source)
44
+ doc = Nokogiri::HTML(source)
45
+ order_items = doc.css(".m-side_orderItems li").map { |item| OrderItem.new(item) }
46
+ OrderItems.new(order_items)
47
+ end
48
+
49
+ def to_s
50
+ map(&:to_s).join("\n")
51
+ end
52
+ end
53
+
54
+ class OrderItem
55
+ attr_accessor :name, :details
56
+
57
+ def initialize(item)
58
+ self.name = item.css(".orderItems_item_name").first.text.strip.gsub(/\s+/, " ")
59
+ # TODO: Get toppings list
60
+
61
+ details_element = item.css(".orderItems_item_detail")
62
+ details_dt = details_element.css("dt").map { |t| t.text.strip.gsub(/\s+/, " ") }
63
+ details_dd = details_element.css("dd").map { |t| t.text.strip.gsub(/\s+/, " ") }
64
+
65
+ self.details = details_dt.zip(details_dd).to_h
66
+ details["Price"] = item.css(".orderItems_item_price").text.strip.gsub(/\s+/, " ")
67
+ end
68
+
69
+ def to_s
70
+ deets = details.map { |key, value| " #{key}: #{value}" }.join("\n")
71
+ "#{name.colorize(:blue)}\n#{deets}"
72
+ end
73
+ end
74
+
75
+ class CouponItems < Array
76
+ def self.from(source)
77
+ doc = Nokogiri::HTML(source)
78
+ coupon_items = doc.css(".m-side_useCoupon li").map { |item| CouponItem.new(item) }
79
+
80
+ CouponItems.new(coupon_items)
81
+ end
82
+
83
+ def to_s
84
+ return unless count.positive?
85
+ "\nCoupons".colorize(:green)
86
+ end
87
+ end
88
+
89
+ class CouponItem
90
+ attr_accessor :name, :value
91
+
92
+ def initialize(item)
93
+ self.name = item.css(".useCoupon_coupons_name").text.strip.gsub(/\s+/, " ").sub("\\", "Β₯")
94
+ self.value = item.css("dd span").text.strip
95
+ end
96
+
97
+ def to_s
98
+ " #{name} #{value.colorize(:green)}"
99
+ end
100
+ end
data/lib/pizza.rb ADDED
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+ class Pizzas < Array
3
+ def self.from(source)
4
+ doc = Nokogiri::HTML(source)
5
+
6
+ Pizzas.new(
7
+ doc.css(".jso-dataLayerProductClick").map { |el| Pizza.new(el) }.select(&:valid?)
8
+ )
9
+ end
10
+
11
+ def selection_list
12
+ map(&:list_item)
13
+ end
14
+ end
15
+
16
+ class Pizza
17
+ attr_accessor :id, :category_id, :url, :name, :description, :allergen_warning
18
+ attr_accessor :size, :crust
19
+
20
+ def initialize(element)
21
+ return unless element["iname"]
22
+
23
+ link = element["href"]
24
+ parts = link.split("/")
25
+ pizza_id = parts.pop
26
+ category_id = parts.pop
27
+ # some_other_number = parts.pop # TODO: figure out what this is
28
+
29
+ description = element.css(".menu_itemList_item_text").first
30
+ description = description.text if description
31
+
32
+ allergen_warning = element.css(".js-menuSetHeight_allergen").first
33
+ allergen_warning = allergen_warning.text if allergen_warning
34
+
35
+ self.url = "https://order.dominos.jp#{link}"
36
+ self.id = pizza_id # shohinC1
37
+ self.category_id = category_id # categoryC
38
+ self.name = element["iname"]
39
+ self.description = description
40
+ self.allergen_warning = allergen_warning
41
+ end
42
+
43
+ def valid?
44
+ url != nil
45
+ end
46
+
47
+ def available_sizes
48
+ @available_sizes ||=
49
+ detail_page_content.css("#detail_selectSize .m-input__radio").map do |option|
50
+ Pizza::Size.new(option)
51
+ end
52
+ end
53
+
54
+ def available_crusts
55
+ @available_crusts ||=
56
+ detail_page_content.css("#detail_selectCrust .m-input__radio").map do |option|
57
+ Pizza::Crust.new(option)
58
+ end
59
+ end
60
+
61
+ def params
62
+ {
63
+ "shohinC1" => id,
64
+ "categoryC" => category_id,
65
+ "sizeC" => size.value,
66
+ "crustC" => crust.value
67
+ }
68
+ end
69
+
70
+ def list_item
71
+ allergen = allergen_warning || ""
72
+
73
+ "#{name.colorize(:blue)} "\
74
+ "#{allergen.strip.colorize(:yellow)}\n "\
75
+ "#{description.gsub(",", ", ").gsub(")", ") ")}\n"
76
+ end
77
+
78
+ private
79
+
80
+ def detail_page_content
81
+ @detail_page_content ||= Nokogiri::HTML(
82
+ Request.get(url, expect: :ok, failure: "Couldn't open pizza detail page").body
83
+ )
84
+ end
85
+ end
86
+
87
+ class Pizza
88
+ class Size
89
+ attr_accessor :text, :value
90
+
91
+ def initialize(option)
92
+ self.text = option.text.strip
93
+ self.value = option.css("input[name=sizeC]").first["value"]
94
+ end
95
+
96
+ def list_item
97
+ text.gsub(/\s+/, " ").sub("δΊΊ /", "δΊΊ/").sub("cm ", "cm\n ").sub(" Β₯", "\n Β₯").strip
98
+ end
99
+ end
100
+
101
+ class Crust
102
+ attr_accessor :text, :value
103
+
104
+ def initialize(option)
105
+ self.text = option.css(".caption_radio").first.text.strip
106
+ self.value = option.css("input[name=crustC]").first["value"]
107
+ end
108
+
109
+ def list_item
110
+ text.gsub(/\s+/, " ").strip
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+ class PizzaSelector
3
+ def self.select_pizzas
4
+ response = Request.get("https://order.dominos.jp/eng/pizza/search/",
5
+ expect: :ok, failure: "Couldn't get pizza list page")
6
+
7
+ pizzas = Pizzas.from(response.body)
8
+
9
+ cli = HighLine.new
10
+ choices = pizzas.selection_list
11
+
12
+ loop do
13
+ puts "-" * 42
14
+ cli.choose do |menu|
15
+ menu.prompt = "Add a pizza via number:"
16
+ menu.choices(*(choices + ["Cancel"])) do |choice|
17
+ index = choices.index(choice)
18
+
19
+ if index && index < choices.count
20
+ selected_pizza = pizzas[index]
21
+ add_pizza(customize_pizza(selected_pizza))
22
+ end
23
+ end
24
+ menu.default = "Cancel"
25
+ end
26
+
27
+ break unless Ask.confirm "Add another pizza?"
28
+ end
29
+ end
30
+
31
+ def self.customize_pizza(pizza)
32
+ puts "#{"β†’".colorize(:green)} #{pizza.name.colorize(:blue)}"
33
+
34
+ # TODO: Allow toppings selection
35
+
36
+ # Choosing the size
37
+ selected_size_index = Ask.list "Choose the size", pizza.available_sizes.map(&:list_item)
38
+ pizza.size = pizza.available_sizes[selected_size_index]
39
+
40
+ # Choosing the crust
41
+ selected_crust_index = Ask.list "Choose the crust", pizza.available_crusts.map(&:list_item)
42
+ pizza.crust = pizza.available_crusts[selected_crust_index]
43
+
44
+ pizza
45
+ end
46
+
47
+ def self.add_pizza(pizza)
48
+ params = pizza.params
49
+ params = params.merge(
50
+ "pageId" => "PIZZA_DETAIL",
51
+ # TODO: Allow cut type, number of slices and quantity selection
52
+ "cutTypeC" => 1, # Type of cut: 1=Round Cut
53
+ "cutSu" => 8, # Number of slices
54
+ "figure" => 1 # Quantity
55
+ )
56
+
57
+ response = Request.post(
58
+ "https://order.dominos.jp/eng/cart/add/pizza/", params,
59
+ expect: :redirect, to: %r{\Ahttps?://order\.dominos\.jp/eng/cart/added/\z},
60
+ failure: "Couldn't add the pizza you selected"
61
+ )
62
+
63
+ # For some reason we need to GET this URL otherwise it doesn't count as added <_<
64
+ Request.get(response["Location"],
65
+ expect: :redirect, to: "https://order.dominos.jp/eng/cart/",
66
+ failure: "Couldn't add the pizza you selected")
67
+ end
68
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ require "yaml"
3
+
4
+ class Preferences
5
+ include Singleton
6
+
7
+ attr_accessor :email, :name, :phone_number, :credit_card, :note
8
+
9
+ def initialize
10
+ preferences_path = File.join(Dir.home, ".dominosjp.yml")
11
+ return unless File.exist?(preferences_path)
12
+
13
+ prefs = YAML.safe_load(File.read(preferences_path)).map { |k, v| [(k.to_sym rescue k), v] }.to_h
14
+
15
+ self.email = prefs[:email] if prefs[:email]
16
+ self.name = prefs[:name] if prefs[:name]
17
+ self.phone_number = prefs[:phone_number] if prefs[:phone_number]
18
+ self.credit_card = CreditCard.new(prefs[:credit_card]) if prefs[:credit_card]
19
+ self.note = prefs[:note] if prefs[:note]
20
+ end
21
+ end
data/lib/request.rb ADDED
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+ require "net/http"
3
+ require "http-cookie"
4
+ require "singleton"
5
+
6
+ class Request
7
+ include Singleton
8
+
9
+ def self.get(url, options = {})
10
+ request = Net::HTTP::Get.new(URI(url))
11
+
12
+ Request.instance.perform(
13
+ request,
14
+ block_given? ? options.merge(proc: proc { |res| yield(res) }) : options
15
+ )
16
+ end
17
+
18
+ def self.post(url, form_data, options = {})
19
+ request = Net::HTTP::Post.new(URI(url))
20
+ request.set_form_data(form_data)
21
+
22
+ Request.instance.perform(
23
+ request,
24
+ block_given? ? options.merge(proc: proc { |res| yield(res) }) : options
25
+ )
26
+ end
27
+
28
+ def initialize
29
+ @base_uri = URI("https://order.dominos.jp/eng/")
30
+ @http = Net::HTTP.start(@base_uri.host, @base_uri.port, use_ssl: true)
31
+ @jar = HTTP::CookieJar.new
32
+ end
33
+
34
+ def perform(request, options)
35
+ request["Cookie"] = cookies_value
36
+ response = @http.request(request)
37
+
38
+ save_cookies(response)
39
+ parse_options(options, response)
40
+
41
+ response
42
+ end
43
+
44
+ private
45
+
46
+ def cookies_value
47
+ HTTP::Cookie.cookie_value(@jar.cookies(@base_uri))
48
+ end
49
+
50
+ def save_cookies(response)
51
+ response.get_fields("Set-Cookie").each do |value|
52
+ @jar.parse(value, @base_uri)
53
+ end
54
+ end
55
+
56
+ def parse_options(options, response)
57
+ validate_status(options, response) if options[:expect]
58
+ validate_block(options, response) if options[:proc]
59
+ end
60
+
61
+ def validate_status(options, response)
62
+ expectation, redirect, failure = options.values_at(:expect, :to, :failure)
63
+ expectation = { ok: 200, redirect: 302 }[expectation] if expectation.is_a?(Symbol)
64
+
65
+ correct_status = (response.code.to_i == expectation)
66
+ correct_location = redirect.nil? ||
67
+ (redirect == response["Location"]) ||
68
+ (redirect.is_a?(Regexp) && redirect.match(response["Location"]))
69
+
70
+ unless correct_status && correct_location
71
+ failure_message =
72
+ failure ||
73
+ "Expected a different server response. "\
74
+ "(expected: #{options} / actual: #{response.code}[#{response["Location"]}])"
75
+
76
+ puts failure_message.colorize(:red)
77
+ raise failure_message
78
+ end
79
+ end
80
+
81
+ def validate_block(options, response)
82
+ expectation, failure = options.values_at(:proc, :failure)
83
+
84
+ unless expectation.call(response)
85
+ failure_message = failure || "Expected a different server response. "
86
+
87
+ puts failure_message.colorize(:red)
88
+ raise failure_message
89
+ end
90
+ end
91
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ class DominosJP
3
+ VERSION = "0.1.0"
4
+ end
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dominosjp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mahdi Bchetnia
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-02-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: colorize
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.8'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.8'
27
+ - !ruby/object:Gem::Dependency
28
+ name: credit_card_validations
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.4'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.4'
41
+ - !ruby/object:Gem::Dependency
42
+ name: highline
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.7'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.7'
55
+ - !ruby/object:Gem::Dependency
56
+ name: http-cookie
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: inquirer
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.2'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: nokogiri
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.7'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.7'
97
+ description: "A ruby gem for ordering Domino's Pizza in Japan via CLI \U0001F355"
98
+ email:
99
+ - injekter@gmail.com
100
+ executables:
101
+ - dominosjp
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - LICENSE
106
+ - README.md
107
+ - bin/dominosjp
108
+ - lib/dominosjp.rb
109
+ - lib/order_address.rb
110
+ - lib/order_coupon.rb
111
+ - lib/order_information.rb
112
+ - lib/order_payment.rb
113
+ - lib/order_review.rb
114
+ - lib/pizza.rb
115
+ - lib/pizza_selector.rb
116
+ - lib/preferences.rb
117
+ - lib/request.rb
118
+ - lib/version.rb
119
+ homepage: https://github.com/inket/dominosjp
120
+ licenses:
121
+ - MIT
122
+ metadata: {}
123
+ post_install_message:
124
+ rdoc_options: []
125
+ require_paths:
126
+ - lib
127
+ required_ruby_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ required_rubygems_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: 1.3.6
137
+ requirements: []
138
+ rubyforge_project: dominosjp
139
+ rubygems_version: 2.5.2
140
+ signing_key:
141
+ specification_version: 4
142
+ summary: "Order Domino's Pizza Japan via CLI \U0001F355"
143
+ test_files: []