def start(self): if not self.token: raise PaymentError( "Cannot start subscription for user without a payment method: %s" % self.user.id) if not self.user.plan_id: raise PaymentError( "Cannot start subscription for user without a confirmed plan: %s" % self.user.id) if not self.user.time_next_payment or self.user.time_next_payment < now( ): # Too early to charge, skip the rest. return plan = self.user.plan amount = plan.price description = "Briefmetrics: %s" % plan.option_str self.invoice(amount, description=description) next_payment = self.user.time_next_payment or now() next_payment += plan.interval self.user.time_next_payment = next_payment
def test_prorate_plan(self): with mock.patch('briefmetrics.api.email.send_message') as send_message: self.assertFalse(send_message.called) resp = self.app.post('/webhook/namecheap', params='{"event_token": "fakecreate"}', content_type='application/json') self.assertTrue(resp.json['response']['state'], 'Active') nc = service_registry['namecheap'].instance # Next payment in the past u = model.User.all()[0] u.num_remaining = None u.time_next_payment = now() - relativedelta(months=6) Session.commit() old_plan = u.plan self.app.post('/webhook/namecheap', params='{"event_token": "fakealter"}', content_type='application/json') new_plan = model.User.get(u.id).plan payments = nc.query(url='/v1/billing/invoice/123/payments') self.assertTrue(payments) line_items = nc.query(url='/v1/billing/invoice/123/line_items') print(line_items) line_item = line_items[0] params = line_item[-1]['json'] self.assertEqual(params['amount'], '%0.2f' % (new_plan.price/100.0)) # Next payment in the future nc.calls[:] = [] # Reset call log u = model.User.all()[0] u.num_remaining = None u.time_next_payment = now() + relativedelta(months=6) Session.commit() old_plan = u.plan self.app.post('/webhook/namecheap', params='{"event_token": "fakealter2"}', content_type='application/json') new_plan = model.User.get(u.id).plan payments = nc.query(url='/v1/billing/invoice/123/payments') self.assertTrue(payments) line_items = nc.query(url='/v1/billing/invoice/123/line_items') print(line_items) line_item = line_items[0] params = line_item[-1]['json'] expected_amount = (new_plan.price - (old_plan.price / 2.0))/100.0 self.assertLess(expected_amount, 0) delta = float(params['amount']) - expected_amount self.assertTrue(abs(delta) < 2, '%s !~= %s' % (params['amount'], expected_amount)) # This will vary by year, so we only approximate
def health(self): errors = [] cutoff = now() - timedelta(hours=1) report = api.report.get_pending(since_time=cutoff, max_num=1, include_new=False).first() if report: errors.append('Report delivery lagged by {}: {}'.format( now() - report.time_next, report)) if errors: raise Exception('\n'.join(errors)) return Response(':)')
def get_pending(since_time=None, max_num=None, include_new=True): since_time = since_time or now() f = (model.Report.time_next <= since_time) if include_new: f |= (model.Report.time_next == None) q = model.Session.query(model.Report).filter(f) if max_num: q = q.limit(max_num) return q
def test_webhook_provison(self): self.assertIn('namecheap', service_registry) self.assertIn('namecheap', payment_registry) # Disable auto-charge restore_auto_charge, payment_registry['namecheap'].auto_charge = payment_registry['namecheap'].auto_charge, False with mock.patch('briefmetrics.api.email.send_message') as send_message: resp = self.app.post('/webhook/namecheap', params='{"event_token": "fakecreate"}', content_type='application/json') r = resp.json self.assertTrue(r['type'], 'subscription_create_resp') self.assertTrue(r['response']['state'], 'Active') self.assertTrue(send_message.called) self.assertEqual(len(send_message.call_args_list), 1) call = send_message.call_args_list[0] message = call[0][1].params self.assertIn(u"Welcome to Briefmetrics", message['subject']) # Check that user was provisioned users = model.User.all() self.assertEqual(len(users), 1) u = users[0] self.assertEqual(u.email, 'foo@localhost') self.assertEqual(u.display_name, 'bar baz') self.assertEqual(u.plan_id, 'starter-yr') self.assertEqual(u.time_next_payment, None) self.assertEqual(u.payment.is_charging, False) p = u.payment self.assertEqual(p.id, 'namecheap') self.assertEqual(p.token, '308') a = u.get_account(service='namecheap') self.assertEqual(a.remote_id, 'testuser') self.app.post('/webhook/namecheap', params='{"event_token": "fakealter"}', content_type='application/json') users = model.User.all() self.assertEqual(len(users), 1) u = users[0] self.assertEqual(u.plan_id, 'agency-10-yr') self.assertEqual(u.time_next_payment, None) u.time_next_payment = now() self.assertEqual(u.payment.is_charging, True) # Restore auto-charge payment_registry['namecheap'].auto_charge = restore_auto_charge
def cleanup(days_retain=1, pretend=False): session = model.Session() until_time = now() - datetime.timedelta(days=days_retain) q = session.query( model.ReportLog).filter(model.ReportLog.time_created < until_time) num = q.count() log.info('Cleaning up %d report logs.' % num) if pretend: return q.delete() session.commit()
def dry_run(num_extra=5, filter_account=None, days_offset=None, is_async=True): q = model.Session.query(model.Report).options( orm.joinedload_all('account.user')) if filter_account: q = q.filter(model.Report.account_id == filter_account) all_reports = q.all() report_queue = all_reports # Start with all paying customers if not filter_account: report_queue = [r for r in all_reports if r.account.user.payment] # Add some extra random customers while num_extra: if len(all_reports) == 0 or len(report_queue) > len(all_reports) * 0.7: # Give up break r = random.choice(all_reports) if r in report_queue: continue report_queue.append(r) num_extra -= 1 if days_offset is None: days_offset = 14 since_time = now() if days_offset: since_time -= datetime.timedelta(days=days_offset) send_fn = send if is_async: send_fn = send.delay log.info('Starting dry run for %d reports.' % len(report_queue)) for report in report_queue: log.info('Dry run for report: %s' % report.display_name) send_fn(report_id=report.id, since_time=_from_datetime(since_time), pretend=True)
def prorate(self, old_plan=None, new_plan=None, since_time=None, time_next_payment=None): if not since_time: since_time = now() if not time_next_payment: time_next_payment = self.user.time_next_payment if not time_next_payment: return 0.0 if not old_plan: old_plan = self.user.plan if not old_plan: return 0.0 amount = (new_plan or old_plan).price if time_next_payment < since_time: return amount last_payment = time_next_payment - old_plan.interval days_paid = (since_time - last_payment).days days_total = (time_next_payment - last_payment).days used = (float(days_paid) / float(days_total)) * old_plan.price amount = used - old_plan.price if new_plan: amount = new_plan.price - used log.debug( "prorate (user_id={user_id}): {old_price} -> {new_price} over {days_paid}/{days_total} = {amount}" .format(user_id=self.user and self.user.id, old_price=old_plan.price, new_price=new_plan and new_plan.price, days_paid=days_paid, days_total=days_total, amount=amount)) return amount
def reschedule(report_id, since_time=None, hour=13, minute=0, second=0, weekday=None): report = model.Report.get(report_id) if not report: raise APIError('report does not exist: %s' % report_id) ReportCls = get_report(report.type) since_time = since_time or now() ctx = ReportCls(report, since_time, None) report.set_time_preferred(hour=hour, minute=minute, second=second, weekday=weekday) report.time_next = ctx.next_preferred(since_time) model.Session.commit() return report
def set(self, new_token=None, metadata=None): if new_token: self.user.set_payment('namecheap', new_token) if not self.user.time_next_payment: self.user.time_next_payment = now()
def view(self): user = api.account.get_user(self.request, required=True, joinedload='accounts') report_id = self.request.matchdict['id'] q = model.Session.query(model.Report).filter_by(id=report_id) if not user.is_admin: q = q.join(model.Report.account).filter_by(user_id=user.id) report = q.first() if not report: raise httpexceptions.HTTPNotFound() config_options = ['pace', 'intro', 'ads', 'bcc'] config = dict((k, v) for k, v in self.request.params.items() if k in config_options and v) # Last Sunday since_time = now() report_context = api.report.fetch(self.request, report, since_time, config=config) template = report_context.template if not report_context.data: template = 'email/error_empty.mako' html = api.email.render(self.request, template, Context({ 'report': report_context, 'user': user, })) is_send = self.request.params.get('send') if is_send: owner = report_context.owner email_kw = {} debug_bcc = config.get('bcc') from_name, from_email, reply_to, api_mandrill_key = get_many(owner.config, optional=['from_name', 'from_email', 'reply_to', 'api_mandrill_key']) if from_name: email_kw['from_name'] = from_name if from_email: email_kw['from_email'] = from_email if reply_to and reply_to != from_email: email_kw['reply_to'] = reply_to if debug_bcc: email_kw['debug_bcc'] = debug_bcc send_kw = {} if api_mandrill_key: send_kw['settings'] = { 'api.mandrill.key': api_mandrill_key, } to_email = user.email message = api.email.create_message(self.request, to_email=to_email, subject=report_context.get_subject(), html=html, **email_kw ) api.email.send_message(self.request, message, **send_kw) self.request.session.flash('Sent report for [%s] to: %s' % (report.display_name, to_email)) return self._redirect(self.request.route_path('reports')) return Response(html)
def _namecheap_subscription_create(request, data): nc_api = service_registry['namecheap'].instance event_id = data['event']['id'] return_uri = data['event'].get('returnURI') subscription_id = data['event']['subscription_id'] try: email, first_name, last_name, remote_id = get_many(data['event']['user'], ['email', 'first_name', 'last_name', 'username']) except KeyError as e: k = e.args[0] log.warning("namecheap webhook: Failing due to missing field '%s': %s" % (k, event_id)) return { 'type': 'subscription_create_resp', 'id': event_id, 'response': { 'state': 'failed', 'provider_id': '', 'message': 'missing field: %s' % k, } } display_name = ' '.join([first_name, last_name]) plan_id = data['event']['order'].get('pricing_plan_sku') user = api.account.get_or_create( email=email, service='namecheap', display_name=display_name, remote_id=remote_id, remote_data=data['event']['user'], plan_id=plan_id, ) if user.num_remaining != 10: log.warning('Resetting num_remaining from %s: %s' % (user.num_remaining, user)) user.num_remaining = 10 ack_message = 'Briefmetrics activation instructions sent to %s' % email ack_state = 'Active' if user.payment: ack_message = 'Failed to provision new Briefmetrics account for {email}. Account already exists with payment information.'.format(email=email) ack_state = 'Failed' log.info('namecheap webhook: Provision skipped %s' % user) else: user.set_payment('namecheap', subscription_id) if user.payment.auto_charge: user.time_next_payment = now() + user.payment.auto_charge model.Session.commit() log.info('namecheap webhook: Provisioned %s' % user) ack = { 'type': 'subscription_create_resp', 'id': event_id, 'response': { 'state': ack_state, 'provider_id': user.id, 'message': ack_message, } } if return_uri: # Confirm event, activate subscription r = nc_api.session.request('PUT', return_uri, json=ack) # Bypass our wrapper assert_response(r) if ack_state != 'Active': return ack subject = u"Welcome to Briefmetrics" html = api.email.render(request, 'email/welcome_namecheap.mako') message = api.email.create_message(request, to_email=email, subject=subject, html=html, ) api.email.send_message(request, message) return ack
def send(request, report, since_time=None, pretend=False, session=model.Session): t = time.time() since_time = since_time or now() if not pretend and report.time_next and report.time_next > since_time: log.warning('send too early, skipping for report: %s' % report.id) return owner = report.account.user if not pretend and report.time_expire and report.time_expire < since_time: if owner.num_remaining is None: # Started paying, un-expire. report.time_expire = None else: log.info('Deleting expired report for %s: %s' % (owner, report)) session.delete(report) session.commit() return send_users = report.users if not send_users: log.warning('No recipients, skipping report: %s' % report.id) return try: report_context = fetch(request, report, since_time) except InvalidGrantError: subject = u"Problem with your Briefmetrics" html = api_email.render(request, 'email/error_auth.mako', Context({ 'report': report, })) message = api_email.create_message( request, to_email=owner.email, subject=subject, html=html, ) if not pretend: api_email.send_message(request, message) report.delete() session.commit() log.warning('Invalid token, removed report: %s' % report) return except APIError as e: if not 'User does not have sufficient permissions for this profile' in e.message: raise subject = u"Problem with your Briefmetrics" html = api_email.render(request, 'email/error_permission.mako', Context({ 'report': report, })) message = api_email.create_message( request, to_email=owner.email, subject=subject, html=html, ) if not pretend: api_email.send_message(request, message) report.delete() model.Session.commit() log.warning('Lost permission to profile, removed report: %s' % report) return messages, force_debug = check_trial(request, report, has_data=report_context.data, pretend=pretend) report_context.messages += messages subject = report_context.get_subject() template = report_context.template time_revive = None if not report_context.data: send_users = [report.account.user] template = 'email/error_empty.mako' time_revive = report_context.next_preferred(report_context.date_end + datetime.timedelta( days=30)) log.info('Sending report to [%d] subscribers: %s' % (len(send_users), report)) debug_sample = float(request.registry.settings.get('mail.debug_sample', 1)) debug_bcc = not report.time_next or random.random() < debug_sample if force_debug: debug_bcc = True email_kw = {} from_name, from_email, reply_to, api_mandrill_key, api_mailgun_key = get_many( owner.config, optional=[ 'from_name', 'from_email', 'reply_to', 'api_mandrill_key', 'api_mailgun_key' ]) if from_name: email_kw['from_name'] = from_name if from_email: email_kw['from_email'] = from_email if reply_to and reply_to != from_email: email_kw['reply_to'] = reply_to send_kw = {} if api_mandrill_key: send_kw['settings'] = { 'api.mandrill.key': api_mandrill_key, } email_kw['mailer'] = 'mandrill' if api_mailgun_key: send_kw['settings'] = { 'api.mailgun.key': api_mailgun_key, } email_kw['mailer'] = 'mailgun' for user in report.users: html = api_email.render( request, template, Context({ 'user': user, 'report': report_context, })) message = api_email.create_message(request, to_email=user.email, subject=subject, html=html, debug_bcc=debug_bcc, **email_kw) if pretend: continue api_email.send_message(request, message, **send_kw) model.ReportLog.create_from_report( report, body=html, subject=subject, seconds_elapsed=time.time() - t, time_sent=None if pretend else now(), ) if pretend: session.commit() return if report_context.data: if owner.num_remaining == 1: subject = u"Your Briefmetrics trial is over" html = api_email.render(request, 'email/error_trial_end.mako') message = api_email.create_message( request, to_email=owner.email, subject=subject, html=html, ) log.info('Sending trial expiration: %s' % owner) if not pretend: api_email.send_message(request, message) if owner.num_remaining: owner.num_remaining -= 1 report.time_last = now() report.time_next = time_revive or report_context.next_preferred( report_context.date_end + datetime.timedelta(days=7)) # XXX: Generalize if owner.num_remaining == 0: report.time_expire = report.time_next session.commit()