| 
									
										
										
										
											2018-06-07 22:35:06 +01:00
										 |  |  | import Stripe from 'stripe'; | 
					
						
							| 
									
										
										
										
											2019-02-07 19:03:18 +05:30
										 |  |  | import debug from 'debug'; | 
					
						
							| 
									
										
										
										
											2019-11-06 19:02:20 +05:30
										 |  |  | import { isEmail, isNumeric } from 'validator'; | 
					
						
							| 
									
										
										
										
											2019-02-07 19:03:18 +05:30
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-13 12:25:57 +03:00
										 |  |  | import { | 
					
						
							|  |  |  |   getAsyncPaypalToken, | 
					
						
							|  |  |  |   verifyWebHook, | 
					
						
							|  |  |  |   updateUser, | 
					
						
							|  |  |  |   verifyWebHookType | 
					
						
							|  |  |  | } from '../utils/donation'; | 
					
						
							| 
									
										
										
										
											2019-11-19 20:30:47 +05:30
										 |  |  | import { | 
					
						
							|  |  |  |   durationKeysConfig, | 
					
						
							| 
									
										
										
										
											2019-12-18 04:15:55 +05:30
										 |  |  |   donationOneTimeConfig, | 
					
						
							| 
									
										
										
										
											2020-03-16 14:32:35 +05:30
										 |  |  |   donationSubscriptionConfig | 
					
						
							| 
									
										
										
										
											2019-11-19 20:30:47 +05:30
										 |  |  | } from '../../../config/donation-settings'; | 
					
						
							| 
									
										
										
										
											2018-08-31 16:04:04 +01:00
										 |  |  | import keys from '../../../config/secrets'; | 
					
						
							| 
									
										
										
										
											2018-06-07 22:35:06 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-02-07 19:03:18 +05:30
										 |  |  | const log = debug('fcc:boot:donate'); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-18 08:47:10 -06:00
										 |  |  | export default function donateBoot(app, done) { | 
					
						
							|  |  |  |   let stripe = false; | 
					
						
							| 
									
										
										
										
											2020-03-21 01:39:29 +05:30
										 |  |  |   const { User } = app.models; | 
					
						
							| 
									
										
										
										
											2018-06-07 22:35:06 +01:00
										 |  |  |   const api = app.loopback.Router(); | 
					
						
							| 
									
										
										
										
											2020-03-19 12:20:04 +05:30
										 |  |  |   const hooks = app.loopback.Router(); | 
					
						
							| 
									
										
										
										
											2018-06-07 22:35:06 +01:00
										 |  |  |   const donateRouter = app.loopback.Router(); | 
					
						
							| 
									
										
										
										
											2019-11-06 19:02:20 +05:30
										 |  |  | 
 | 
					
						
							|  |  |  |   const subscriptionPlans = Object.keys( | 
					
						
							|  |  |  |     donationSubscriptionConfig.plans | 
					
						
							|  |  |  |   ).reduce( | 
					
						
							|  |  |  |     (prevDuration, duration) => | 
					
						
							|  |  |  |       prevDuration.concat( | 
					
						
							|  |  |  |         donationSubscriptionConfig.plans[duration].reduce( | 
					
						
							|  |  |  |           (prevAmount, amount) => | 
					
						
							|  |  |  |             prevAmount.concat({ | 
					
						
							|  |  |  |               amount: amount, | 
					
						
							|  |  |  |               interval: duration, | 
					
						
							|  |  |  |               product: { | 
					
						
							|  |  |  |                 name: `${ | 
					
						
							|  |  |  |                   donationSubscriptionConfig.duration[duration] | 
					
						
							|  |  |  |                 } Donation to freeCodeCamp.org - Thank you ($${amount / 100})`,
 | 
					
						
							|  |  |  |                 metadata: { | 
					
						
							|  |  |  |                   /* eslint-disable camelcase */ | 
					
						
							|  |  |  |                   sb_service: `freeCodeCamp.org`, | 
					
						
							|  |  |  |                   sb_tier: `${ | 
					
						
							|  |  |  |                     donationSubscriptionConfig.duration[duration] | 
					
						
							|  |  |  |                   } $${amount / 100} Donation`
 | 
					
						
							|  |  |  |                   /* eslint-enable camelcase */ | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |               }, | 
					
						
							|  |  |  |               currency: 'usd', | 
					
						
							|  |  |  |               id: `${donationSubscriptionConfig.duration[ | 
					
						
							|  |  |  |                 duration | 
					
						
							|  |  |  |               ].toLowerCase()}-donation-${amount}`
 | 
					
						
							|  |  |  |             }), | 
					
						
							|  |  |  |           [] | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |       ), | 
					
						
							|  |  |  |     [] | 
					
						
							| 
									
										
										
										
											2018-06-18 08:47:10 -06:00
										 |  |  |   ); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-11-06 19:02:20 +05:30
										 |  |  |   function validStripeForm(amount, duration, email) { | 
					
						
							|  |  |  |     return isEmail('' + email) && | 
					
						
							|  |  |  |       isNumeric('' + amount) && | 
					
						
							| 
									
										
										
										
											2019-11-19 20:30:47 +05:30
										 |  |  |       durationKeysConfig.includes(duration) && | 
					
						
							| 
									
										
										
										
											2019-11-06 19:02:20 +05:30
										 |  |  |       duration === 'onetime' | 
					
						
							| 
									
										
										
										
											2019-12-18 04:15:55 +05:30
										 |  |  |       ? donationOneTimeConfig.includes(amount) | 
					
						
							| 
									
										
										
										
											2019-11-06 19:02:20 +05:30
										 |  |  |       : donationSubscriptionConfig.plans[duration]; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-18 08:47:10 -06:00
										 |  |  |   function connectToStripe() { | 
					
						
							|  |  |  |     return new Promise(function(resolve) { | 
					
						
							|  |  |  |       // connect to stripe API
 | 
					
						
							|  |  |  |       stripe = Stripe(keys.stripe.secret); | 
					
						
							|  |  |  |       // parse stripe plans
 | 
					
						
							| 
									
										
										
										
											2019-11-06 19:02:20 +05:30
										 |  |  |       stripe.plans.list({}, function(err, stripePlans) { | 
					
						
							| 
									
										
										
										
											2018-06-18 08:47:10 -06:00
										 |  |  |         if (err) { | 
					
						
							|  |  |  |           throw err; | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2019-11-06 19:02:20 +05:30
										 |  |  |         const requiredPlans = subscriptionPlans.map(plan => plan.id); | 
					
						
							|  |  |  |         const availablePlans = stripePlans.data.map(plan => plan.id); | 
					
						
							| 
									
										
										
										
											2019-11-19 14:48:09 +05:30
										 |  |  |         if (process.env.STRIPE_CREATE_PLANS === 'true') { | 
					
						
							|  |  |  |           requiredPlans.forEach(requiredPlan => { | 
					
						
							|  |  |  |             if (!availablePlans.includes(requiredPlan)) { | 
					
						
							|  |  |  |               createStripePlan( | 
					
						
							|  |  |  |                 subscriptionPlans.find(plan => plan.id === requiredPlan) | 
					
						
							|  |  |  |               ); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |           }); | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |           log(`Skipping plan creation`); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2018-06-18 08:47:10 -06:00
										 |  |  |       }); | 
					
						
							|  |  |  |       resolve(); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   function createStripePlan(plan) { | 
					
						
							| 
									
										
										
										
											2019-11-06 19:02:20 +05:30
										 |  |  |     log(`Creating subscription plan: ${plan.product.name}`); | 
					
						
							| 
									
										
										
										
											2018-06-18 08:47:10 -06:00
										 |  |  |     stripe.plans.create(plan, function(err) { | 
					
						
							|  |  |  |       if (err) { | 
					
						
							| 
									
										
										
										
											2019-11-06 19:02:20 +05:30
										 |  |  |         log(err); | 
					
						
							| 
									
										
										
										
											2018-06-18 08:47:10 -06:00
										 |  |  |       } | 
					
						
							| 
									
										
										
										
											2019-11-06 19:02:20 +05:30
										 |  |  |       log(`Created plan with plan id: ${plan.id}`); | 
					
						
							| 
									
										
										
										
											2019-02-06 14:19:58 +00:00
										 |  |  |       return; | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2018-06-18 08:47:10 -06:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2018-06-07 22:35:06 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |   function createStripeDonation(req, res) { | 
					
						
							|  |  |  |     const { user, body } = req; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-02-18 19:32:49 +00:00
										 |  |  |     const { | 
					
						
							|  |  |  |       amount, | 
					
						
							| 
									
										
										
										
											2019-11-06 19:02:20 +05:30
										 |  |  |       duration, | 
					
						
							| 
									
										
										
										
											2019-02-18 19:32:49 +00:00
										 |  |  |       token: { email, id } | 
					
						
							|  |  |  |     } = body; | 
					
						
							| 
									
										
										
										
											2018-06-07 22:35:06 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-11-06 19:02:20 +05:30
										 |  |  |     if (!validStripeForm(amount, duration, email)) { | 
					
						
							| 
									
										
										
										
											2019-11-14 01:33:53 +05:30
										 |  |  |       return res.status(500).send({ | 
					
						
							|  |  |  |         error: 'The donation form had invalid values for this submission.' | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2019-11-06 19:02:20 +05:30
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-21 01:39:29 +05:30
										 |  |  |     const fccUser = user | 
					
						
							|  |  |  |       ? Promise.resolve(user) | 
					
						
							|  |  |  |       : new Promise((resolve, reject) => | 
					
						
							|  |  |  |           User.findOrCreate( | 
					
						
							|  |  |  |             { where: { email } }, | 
					
						
							|  |  |  |             { email }, | 
					
						
							|  |  |  |             (err, instance, isNew) => { | 
					
						
							|  |  |  |               log('createing a new donating user instance: ', isNew); | 
					
						
							|  |  |  |               if (err) { | 
					
						
							|  |  |  |                 return reject(err); | 
					
						
							|  |  |  |               } | 
					
						
							|  |  |  |               return resolve(instance); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |           ) | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-07 22:35:06 +01:00
										 |  |  |     let donatingUser = {}; | 
					
						
							|  |  |  |     let donation = { | 
					
						
							|  |  |  |       email, | 
					
						
							|  |  |  |       amount, | 
					
						
							| 
									
										
										
										
											2019-11-06 19:02:20 +05:30
										 |  |  |       duration, | 
					
						
							| 
									
										
										
										
											2018-06-07 22:35:06 +01:00
										 |  |  |       provider: 'stripe', | 
					
						
							|  |  |  |       startDate: new Date(Date.now()).toISOString() | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-11-06 19:02:20 +05:30
										 |  |  |     const createCustomer = user => { | 
					
						
							|  |  |  |       donatingUser = user; | 
					
						
							|  |  |  |       return stripe.customers.create({ | 
					
						
							|  |  |  |         email, | 
					
						
							|  |  |  |         card: id | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const createSubscription = customer => { | 
					
						
							|  |  |  |       donation.customerId = customer.id; | 
					
						
							|  |  |  |       return stripe.subscriptions.create({ | 
					
						
							|  |  |  |         customer: customer.id, | 
					
						
							|  |  |  |         items: [ | 
					
						
							|  |  |  |           { | 
					
						
							|  |  |  |             plan: `${donationSubscriptionConfig.duration[ | 
					
						
							|  |  |  |               duration | 
					
						
							|  |  |  |             ].toLowerCase()}-donation-${amount}`
 | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         ] | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const createOneTimeCharge = customer => { | 
					
						
							|  |  |  |       donation.customerId = customer.id; | 
					
						
							|  |  |  |       return stripe.charges.create({ | 
					
						
							|  |  |  |         amount: amount, | 
					
						
							|  |  |  |         currency: 'usd', | 
					
						
							|  |  |  |         customer: customer.id | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const createAsyncUserDonation = () => { | 
					
						
							|  |  |  |       donatingUser | 
					
						
							|  |  |  |         .createDonation(donation) | 
					
						
							|  |  |  |         .toPromise() | 
					
						
							|  |  |  |         .catch(err => { | 
					
						
							|  |  |  |           throw new Error(err); | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-21 01:39:29 +05:30
										 |  |  |     return Promise.resolve(fccUser) | 
					
						
							| 
									
										
										
										
											2020-01-09 02:37:50 +05:30
										 |  |  |       .then(nonDonatingUser => { | 
					
						
							|  |  |  |         const { isDonating } = nonDonatingUser; | 
					
						
							| 
									
										
										
										
											2020-03-21 01:39:29 +05:30
										 |  |  |         if (isDonating && duration !== 'onetime') { | 
					
						
							| 
									
										
										
										
											2020-01-09 02:37:50 +05:30
										 |  |  |           throw { | 
					
						
							| 
									
										
										
										
											2020-03-21 01:39:29 +05:30
										 |  |  |             message: `User already has active recurring donation(s).`, | 
					
						
							| 
									
										
										
										
											2020-01-09 02:37:50 +05:30
										 |  |  |             type: 'AlreadyDonatingError' | 
					
						
							|  |  |  |           }; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         return nonDonatingUser; | 
					
						
							|  |  |  |       }) | 
					
						
							| 
									
										
										
										
											2019-11-06 19:02:20 +05:30
										 |  |  |       .then(createCustomer) | 
					
						
							| 
									
										
										
										
											2019-02-06 14:19:58 +00:00
										 |  |  |       .then(customer => { | 
					
						
							| 
									
										
										
										
											2019-11-14 01:33:53 +05:30
										 |  |  |         return duration === 'onetime' | 
					
						
							| 
									
										
										
										
											2019-11-06 19:02:20 +05:30
										 |  |  |           ? createOneTimeCharge(customer).then(charge => { | 
					
						
							|  |  |  |               donation.subscriptionId = 'one-time-charge-prefix-' + charge.id; | 
					
						
							|  |  |  |               return res.send(charge); | 
					
						
							|  |  |  |             }) | 
					
						
							|  |  |  |           : createSubscription(customer).then(subscription => { | 
					
						
							|  |  |  |               donation.subscriptionId = subscription.id; | 
					
						
							|  |  |  |               return res.send(subscription); | 
					
						
							|  |  |  |             }); | 
					
						
							| 
									
										
										
										
											2019-02-06 14:19:58 +00:00
										 |  |  |       }) | 
					
						
							| 
									
										
										
										
											2019-11-06 19:02:20 +05:30
										 |  |  |       .then(createAsyncUserDonation) | 
					
						
							| 
									
										
										
										
											2019-02-06 14:19:58 +00:00
										 |  |  |       .catch(err => { | 
					
						
							| 
									
										
										
										
											2020-01-09 02:37:50 +05:30
										 |  |  |         if ( | 
					
						
							|  |  |  |           err.type === 'StripeCardError' || | 
					
						
							|  |  |  |           err.type === 'AlreadyDonatingError' | 
					
						
							|  |  |  |         ) { | 
					
						
							| 
									
										
										
										
											2019-12-18 04:15:55 +05:30
										 |  |  |           return res.status(402).send({ error: err.message }); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         return res | 
					
						
							|  |  |  |           .status(500) | 
					
						
							|  |  |  |           .send({ error: 'Donation failed due to a server error.' }); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-13 12:25:57 +03:00
										 |  |  |   function addDonation(req, res) { | 
					
						
							|  |  |  |     const { user, body } = req; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (!user || !body) { | 
					
						
							|  |  |  |       return res | 
					
						
							|  |  |  |         .status(500) | 
					
						
							|  |  |  |         .send({ error: 'User must be signed in for this request.' }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return Promise.resolve(req) | 
					
						
							|  |  |  |       .then( | 
					
						
							|  |  |  |         user.updateAttributes({ | 
					
						
							|  |  |  |           isDonating: true | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  |       ) | 
					
						
							|  |  |  |       .then(() => res.status(200).json({ isDonating: true })) | 
					
						
							|  |  |  |       .catch(err => { | 
					
						
							|  |  |  |         log(err.message); | 
					
						
							|  |  |  |         return res.status(500).send({ | 
					
						
							|  |  |  |           type: 'danger', | 
					
						
							|  |  |  |           message: 'Something went wrong.' | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   function updatePaypal(req, res) { | 
					
						
							|  |  |  |     const { headers, body } = req; | 
					
						
							|  |  |  |     return Promise.resolve(req) | 
					
						
							|  |  |  |       .then(verifyWebHookType) | 
					
						
							|  |  |  |       .then(getAsyncPaypalToken) | 
					
						
							| 
									
										
										
										
											2020-03-16 14:32:35 +05:30
										 |  |  |       .then(token => verifyWebHook(headers, body, token, keys.paypal.webhookId)) | 
					
						
							| 
									
										
										
										
											2020-03-13 12:25:57 +03:00
										 |  |  |       .then(hookBody => updateUser(hookBody, app)) | 
					
						
							|  |  |  |       .catch(err => { | 
					
						
							| 
									
										
										
										
											2020-03-19 12:20:04 +05:30
										 |  |  |         // Todo: This probably need to be thrown and caught in error handler
 | 
					
						
							| 
									
										
										
										
											2020-03-13 12:25:57 +03:00
										 |  |  |         log(err.message); | 
					
						
							| 
									
										
										
										
											2020-03-19 12:20:04 +05:30
										 |  |  |       }) | 
					
						
							|  |  |  |       .finally(() => res.status(200).json({ message: 'received paypal hook' })); | 
					
						
							| 
									
										
										
										
											2020-03-13 12:25:57 +03:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2019-11-13 19:40:49 +05:30
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-13 12:25:57 +03:00
										 |  |  |   const stripeKey = keys.stripe.public; | 
					
						
							| 
									
										
										
										
											2018-06-18 08:47:10 -06:00
										 |  |  |   const secKey = keys.stripe.secret; | 
					
						
							| 
									
										
										
										
											2020-03-13 12:25:57 +03:00
										 |  |  |   const paypalKey = keys.paypal.client; | 
					
						
							|  |  |  |   const paypalSec = keys.paypal.secret; | 
					
						
							|  |  |  |   const stripeSecretInvalid = !secKey || secKey === 'sk_from_stripe_dashboard'; | 
					
						
							|  |  |  |   const stripPublicInvalid = | 
					
						
							|  |  |  |     !stripeKey || stripeKey === 'pk_from_stripe_dashboard'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const paypalSecretInvalid = | 
					
						
							|  |  |  |     !paypalKey || paypalKey === 'id_from_paypal_dashboard'; | 
					
						
							|  |  |  |   const paypalPublicInvalid = | 
					
						
							|  |  |  |     !paypalSec || paypalSec === 'secret_from_paypal_dashboard'; | 
					
						
							| 
									
										
										
										
											2020-08-26 15:51:56 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-13 12:25:57 +03:00
										 |  |  |   const paypalInvalid = paypalPublicInvalid || paypalSecretInvalid; | 
					
						
							|  |  |  |   const stripeInvalid = stripeSecretInvalid || stripPublicInvalid; | 
					
						
							| 
									
										
										
										
											2020-03-16 14:32:35 +05:30
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-26 15:51:56 +02:00
										 |  |  |   if (stripeInvalid || paypalInvalid) { | 
					
						
							| 
									
										
										
										
											2019-08-19 01:19:40 +05:30
										 |  |  |     if (process.env.FREECODECAMP_NODE_ENV === 'production') { | 
					
						
							| 
									
										
										
										
											2020-03-16 14:32:35 +05:30
										 |  |  |       throw new Error('Donation API keys are required to boot the server!'); | 
					
						
							| 
									
										
										
										
											2018-06-18 08:47:10 -06:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2020-03-16 14:32:35 +05:30
										 |  |  |     log('Donation disabled in development unless ALL test keys are provided'); | 
					
						
							|  |  |  |     done(); | 
					
						
							| 
									
										
										
										
											2018-06-18 08:47:10 -06:00
										 |  |  |   } else { | 
					
						
							|  |  |  |     api.post('/charge-stripe', createStripeDonation); | 
					
						
							| 
									
										
										
										
											2020-03-13 12:25:57 +03:00
										 |  |  |     api.post('/add-donation', addDonation); | 
					
						
							| 
									
										
										
										
											2020-03-19 12:20:04 +05:30
										 |  |  |     hooks.post('/update-paypal', updatePaypal); | 
					
						
							| 
									
										
										
										
											2020-03-16 14:32:35 +05:30
										 |  |  |     donateRouter.use('/donate', api); | 
					
						
							| 
									
										
										
										
											2020-03-19 12:20:04 +05:30
										 |  |  |     donateRouter.use('/hooks', hooks); | 
					
						
							| 
									
										
										
										
											2020-03-16 14:32:35 +05:30
										 |  |  |     app.use(donateRouter); | 
					
						
							| 
									
										
										
										
											2018-06-18 08:47:10 -06:00
										 |  |  |     connectToStripe().then(done); | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2018-06-07 22:35:06 +01:00
										 |  |  | } |