Esempio n. 1
0
    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
Esempio n. 2
0
    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
Esempio n. 3
0
    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(':)')
Esempio n. 4
0
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
Esempio n. 5
0
    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
Esempio n. 6
0
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()
Esempio n. 7
0
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)
Esempio n. 8
0
    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
Esempio n. 9
0
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
Esempio n. 10
0
 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()
Esempio n. 11
0
    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)
Esempio n. 12
0
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
Esempio n. 13
0
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()