]> git.ipfire.org Git - ipfire.org.git/blob - src/web/donate.py
donate: Add backend for handling organizations
[ipfire.org.git] / src / web / donate.py
1 #!/usr/bin/python3
2
3 import iso3166
4 import tornado.web
5
6 from . import auth
7 from . import base
8
9 SKUS = {
10 "monthly" : "IPFIRE-DONATION-MONTHLY",
11 "quarterly" : "IPFIRE-DONATION-QUARTERLY",
12 "yearly" : "IPFIRE-DONATION-YEARLY",
13 }
14 DEFAULT_SKU = "IPFIRE-DONATION"
15
16 class DonateHandler(auth.CacheMixin, base.BaseHandler):
17 def get(self):
18 country = self.current_country_code
19
20 # Get defaults
21 first_name = self.get_argument("first_name", None)
22 last_name = self.get_argument("last_name", None)
23 amount = self.get_argument_float("amount", None)
24 currency = self.get_argument("currency", None)
25 frequency = self.get_argument("frequency", None)
26
27 # Set default currency
28 if not currency in ("EUR", "USD"):
29 currency = "EUR"
30
31 # Default to USD for the US only
32 if country == "US":
33 currency = "USD"
34
35 # Set default frequency
36 if not frequency in ("one-time", "monthly"):
37 frequency = "one-time"
38
39 self.render("donate/donate.html", countries=iso3166.countries,
40 country=country, first_name=first_name, last_name=last_name,
41 amount=amount, currency=currency, frequency=frequency)
42
43 @base.ratelimit(minutes=15, requests=5)
44 async def post(self):
45 type = self.get_argument("type")
46 if not type in ("individual", "organization"):
47 raise tornado.web.HTTPError(400, "type is of an invalid value: %s" % type)
48
49 amount = self.get_argument("amount")
50 currency = self.get_argument("currency", "EUR")
51 frequency = self.get_argument("frequency")
52
53 organization = None
54
55 # Get organization information
56 if type == "organization":
57 organization = {
58 "name" : self.get_argument("organization"),
59 "vat_number" : self.get_argument("vat_number", None),
60 }
61
62 # Collect person information
63 person = {
64 "email" : self.get_argument("email"),
65 "title" : self.get_argument("title"),
66 "first_name" : self.get_argument("first_name"),
67 "last_name" : self.get_argument("last_name"),
68 }
69
70 # Collect address information
71 address = {
72 "street1" : self.get_argument("street1"),
73 "street2" : self.get_argument("street2", None),
74 "post_code" : self.get_argument("post_code"),
75 "city" : self.get_argument("city"),
76 "state" : self.get_argument("state", None),
77 "country_code" : self.get_argument("country_code"),
78 }
79
80 # Send everything to Zeiterfassung
81 try:
82 # Create a new organization
83 if organization:
84 organization = await self._create_organization(organization, address)
85
86 # Create a person
87 person = await self._create_person(person, address, organization)
88
89 # Create a new order
90 order = await self._create_order(person=person, currency=currency)
91
92 # Add donation to the order
93 await self._create_donation(order, frequency, amount, currency,
94 vat_included=(type == "individual"))
95
96 # Submit the order
97 needs_payment = await self._submit_order(order)
98
99 # Pay the order
100 if needs_payment:
101 redirect_url = await self._pay_order(order)
102 else:
103 redirect_url = "https://%s/donate/thank-you" % self.request.host
104
105 # Redirect the user to the payment page
106 if not redirect_url:
107 raise tornado.web.HTTPError(500, "Did not receive a redirect URL")
108
109 self.redirect(redirect_url)
110
111 # XXX handle any problems when Zeiterfassung is unreachable
112 except Exception:
113 raise
114
115 async def _create_organization(self, organization, address):
116 # Check if we have an existing organization
117 response = await self.backend.zeiterfassung.send_request(
118 "/api/v1/organizations/search", **organization,
119 )
120
121 # Update details if we found a match
122 if response:
123 number = response.get("number")
124
125 # Update name
126 await self.backend.zeiterfassung.send_request(
127 "/api/v1/organizations/%s/name" % number, **organization
128 )
129
130 # Update VAT number
131 vat_number = organization.get("vat_number", None)
132 if vat_number:
133 await self.backend.zeiterfassung.send_request(
134 "/api/v1/organizations/%s/vat-number" % number, vat_number=vat_number,
135 )
136
137 # Update address
138 await self.backend.zeiterfassung.send_request(
139 "/api/v1/organizations/%s/address" % number, **address,
140 )
141
142 return number
143
144 # Otherwise we will create a new one
145 response = await self.backend.zeiterfassung.send_request(
146 "/api/v1/organizations/create", **organization, **address,
147 )
148
149 # Return the organization's number
150 return response.get("number")
151
152 async def _create_person(self, person, address, organization=None):
153 """
154 Searches for a matching person or creates a new one
155 """
156 # Check if we have an existing person
157 response = await self.backend.zeiterfassung.send_request(
158 "/api/v1/persons/search", **person
159 )
160
161 # Update details if we found a match
162 if response:
163 number = response.get("number")
164
165 # Update name
166 await self.backend.zeiterfassung.send_request(
167 "/api/v1/persons/%s/name" % number, **person,
168 )
169
170 # Update address
171 await self.backend.zeiterfassung.send_request(
172 "/api/v1/persons/%s/address" % number, **address,
173 )
174
175 return number
176
177 # If not, we will create a new one
178 response = await self.backend.zeiterfassung.send_request(
179 "/api/v1/persons/create", organization=organization, **person, **address
180 )
181
182 # Return the persons's number
183 return response.get("number")
184
185 async def _create_order(self, person, currency=None):
186 """
187 Creates a new order and returns its ID
188 """
189 response = await self.backend.zeiterfassung.send_request(
190 "/api/v1/orders/create", person=person, currency=currency,
191 )
192
193 # Return the order number
194 return response.get("number")
195
196 async def _create_donation(self, order, frequency, amount, currency,
197 vat_included=False):
198 """
199 Creates a new donation
200 """
201 # Select the correct product
202 try:
203 sku = SKUS[frequency]
204 except KeyError:
205 sku = DEFAULT_SKU
206
207 # Add it to the order
208 await self.backend.zeiterfassung.send_request(
209 "/api/v1/orders/%s/products/add" % order, sku=sku, quantity=1,
210 )
211
212 # Set the price
213 await self.backend.zeiterfassung.send_request(
214 "/api/v1/orders/%s/products/%s/price" % (order, sku),
215 price=amount, currency=currency, vat_included=vat_included,
216 )
217
218 async def _submit_order(self, order):
219 """
220 Submits the order
221 """
222 response = await self.backend.zeiterfassung.send_request(
223 "/api/v1/orders/%s/submit" % order,
224 )
225
226 # Return whether this needs payment
227 return not response.get("is_authorized")
228
229 async def _pay_order(self, order):
230 """
231 Pay the order
232 """
233 # Add URLs to redirect the user back
234 urls = {
235 "success_url" : "https://%s/donate/thank-you" % self.request.host,
236 "error_url" : "https://%s/donate/error" % self.request.host,
237 "back_url" : "https://%s/donate" % self.request.host,
238 }
239
240 # Send request
241 response = await self.backend.zeiterfassung.send_request(
242 "/api/v1/orders/%s/pay" % order, **urls,
243 )
244
245 # Return redirect URL
246 return response.get("redirect_url", None)
247
248
249 class ThankYouHandler(base.BaseHandler):
250 def get(self):
251 self.render("donate/thank-you.html")
252
253
254 class ErrorHandler(base.BaseHandler):
255 def get(self):
256 self.render("donate/error.html")