Example #1
0
    def test_get_subscription_of_other_company(self):
        from billy.models.company import CompanyModel
        from billy.models.customer import CustomerModel
        from billy.models.plan import PlanModel

        company_model = CompanyModel(self.testapp.session)
        customer_model = CustomerModel(self.testapp.session)
        plan_model = PlanModel(self.testapp.session)
        with db_transaction.manager:
            other_company_guid = company_model.create(processor_key="MOCK_PROCESSOR_KEY")
            other_customer_guid = customer_model.create(company_guid=other_company_guid)
            other_plan_guid = plan_model.create(
                company_guid=other_company_guid,
                frequency=plan_model.FREQ_WEEKLY,
                plan_type=plan_model.TYPE_CHARGE,
                amount=10,
            )
        other_company = company_model.get(other_company_guid)
        other_api_key = str(other_company.api_key)

        res = self.testapp.post(
            "/v1/subscriptions",
            dict(customer_guid=other_customer_guid, plan_guid=other_plan_guid),
            extra_environ=dict(REMOTE_USER=other_api_key),
            status=200,
        )
        other_guid = res.json["guid"]

        self.testapp.get(
            "/v1/subscriptions/{}".format(other_guid), extra_environ=dict(REMOTE_USER=self.api_key), status=403
        )
Example #2
0
 def setUp(self):
     from billy.models.company import CompanyModel
     from billy.models.customer import CustomerModel
     from billy.models.plan import PlanModel
     self.settings = {
         'billy.processor_factory': DummyProcessor
     }
     super(TestSubscriptionViews, self).setUp()
     company_model = CompanyModel(self.testapp.session)
     customer_model = CustomerModel(self.testapp.session)
     plan_model = PlanModel(self.testapp.session)
     with db_transaction.manager:
         self.company_guid = company_model.create(
             processor_key='MOCK_PROCESSOR_KEY',
         )
         self.customer_guid = customer_model.create(
             company_guid=self.company_guid
         )
         self.plan_guid = plan_model.create(
             company_guid=self.company_guid,
             frequency=plan_model.FREQ_WEEKLY,
             plan_type=plan_model.TYPE_CHARGE,
             amount=10,
         )
     company = company_model.get(self.company_guid)
     self.api_key = str(company.api_key)
Example #3
0
def subscription_list_post(request):
    """Create a new subscription 

    """
    company = auth_api_key(request)
    form = validate_form(SubscriptionCreateForm, request)

    customer_guid = form.data['customer_guid']
    plan_guid = form.data['plan_guid']
    amount = form.data.get('amount')
    payment_uri = form.data.get('payment_uri')
    if not payment_uri:
        payment_uri = None
    started_at = form.data.get('started_at')
    maximum_retry = int(request.registry.settings.get(
        'billy.transaction.maximum_retry', 
        TransactionModel.DEFAULT_MAXIMUM_RETRY,
    ))

    model = SubscriptionModel(request.session)
    plan_model = PlanModel(request.session)
    customer_model = CustomerModel(request.session)
    tx_model = TransactionModel(request.session)

    customer = customer_model.get(customer_guid)
    if customer.company_guid != company.guid:
        return HTTPForbidden('Can only subscribe to your own customer')
    if customer.deleted:
        return HTTPBadRequest('Cannot subscript to a deleted customer')
    plan = plan_model.get(plan_guid)
    if plan.company_guid != company.guid:
        return HTTPForbidden('Can only subscribe to your own plan')
    if plan.deleted:
        return HTTPBadRequest('Cannot subscript to a deleted plan')

    # create subscription and yield transactions
    with db_transaction.manager:
        guid = model.create(
            customer_guid=customer_guid, 
            plan_guid=plan_guid, 
            amount=amount, 
            payment_uri=payment_uri,
            started_at=started_at, 
        )
        tx_guids = model.yield_transactions([guid])
    # this is not a deferred subscription, just process transactions right away
    if started_at is None:
        with db_transaction.manager:
            tx_model.process_transactions(
                processor=request.processor, 
                guids=tx_guids,
                maximum_retry=maximum_retry,
            )

    subscription = model.get(guid)
    return subscription
Example #4
0
def get_and_check_customer(request, company):
    """Get and check permission to access a customer

    """
    model = CustomerModel(request.session)
    guid = request.matchdict['customer_guid']
    customer = model.get(guid)
    if customer is None:
        raise HTTPNotFound('No such customer {}'.format(guid))
    if customer.company_guid != company.guid:
        raise HTTPForbidden('You have no permission to access customer {}'
                            .format(guid))
    return customer 
Example #5
0
def customer_get(request):
    """Get and return a customer 

    """
    company = auth_api_key(request)
    model = CustomerModel(request.session)
    guid = request.matchdict['customer_guid']
    customer = model.get(guid)
    if customer is None:
        return HTTPNotFound('No such customer {}'.format(guid))
    if customer.company_guid != company.guid:
        return HTTPForbidden('You have no permission to access customer {}'
                             .format(guid))
    return customer 
Example #6
0
def customer_delete(request):
    """Delete and return customer

    """
    company = auth_api_key(request)
    model = CustomerModel(request.session)
    customer = get_and_check_customer(request, company)
    if customer.deleted:
        return HTTPBadRequest('Customer {} was already deleted'
                              .format(customer.guid))
    with db_transaction.manager:
        model.delete(customer.guid)
    customer = model.get(customer.guid)
    return customer
Example #7
0
    def test_create_subscription_to_a_deleted_customer(self):
        from billy.models.customer import CustomerModel

        customer_model = CustomerModel(self.testapp.session)

        with db_transaction.manager:
            customer_guid = customer_model.create(company_guid=self.company_guid)
            customer_model.delete(customer_guid)

        self.testapp.post(
            "/v1/subscriptions",
            dict(customer_guid=customer_guid, plan_guid=self.plan_guid, amount="123", payment_uri="MOCK_CARD_URI"),
            extra_environ=dict(REMOTE_USER=self.api_key),
            status=400,
        )
Example #8
0
 def test_customer(self):
     from billy.models.customer import CustomerModel
     from billy.renderers import customer_adapter
     customer_model = CustomerModel(self.testapp.session)
     customer = customer_model.get(self.customer_guid)
     json_data = customer_adapter(customer, self.dummy_request)
     expected = dict(
         guid=customer.guid,
         external_id=customer.external_id, 
         created_at=customer.created_at.isoformat(),
         updated_at=customer.updated_at.isoformat(),
         company_guid=customer.company_guid, 
         deleted=customer.deleted, 
     )
     self.assertEqual(json_data, expected)
Example #9
0
 def setUp(self):
     from billy.models.company import CompanyModel
     from billy.models.customer import CustomerModel
     from billy.models.plan import PlanModel
     super(TestSubscriptionModel, self).setUp()
     # build the basic scenario for plan model
     self.company_model = CompanyModel(self.session)
     self.customer_model = CustomerModel(self.session)
     self.plan_model = PlanModel(self.session)
     with db_transaction.manager:
         self.company_guid = self.company_model.create('my_secret_key')
         self.daily_plan_guid = self.plan_model.create(
             company_guid=self.company_guid,
             plan_type=self.plan_model.TYPE_CHARGE,
             amount=1000,  # 10 dollars
             frequency=self.plan_model.FREQ_DAILY,
         )
         self.weekly_plan_guid = self.plan_model.create(
             company_guid=self.company_guid,
             plan_type=self.plan_model.TYPE_CHARGE,
             amount=1000,
             frequency=self.plan_model.FREQ_WEEKLY,
         )
         self.monthly_plan_guid = self.plan_model.create(
             company_guid=self.company_guid,
             plan_type=self.plan_model.TYPE_CHARGE,
             amount=1000,
             frequency=self.plan_model.FREQ_MONTHLY,
         )
         self.customer_tom_guid = self.customer_model.create(
             company_guid=self.company_guid,
         )
Example #10
0
 def setUp(self):
     from billy.models.company import CompanyModel
     from billy.models.customer import CustomerModel
     from billy.models.plan import PlanModel
     from billy.models.subscription import SubscriptionModel
     from billy.models.transaction import TransactionModel
     super(TestBalancedProcessorModel, self).setUp()
     # build the basic scenario for transaction model
     self.company_model = CompanyModel(self.session)
     self.customer_model = CustomerModel(self.session)
     self.plan_model = PlanModel(self.session)
     self.subscription_model = SubscriptionModel(self.session)
     self.transaction_model = TransactionModel(self.session)
     with db_transaction.manager:
         self.company_guid = self.company_model.create('my_secret_key')
         self.plan_guid = self.plan_model.create(
             company_guid=self.company_guid,
             plan_type=self.plan_model.TYPE_CHARGE,
             amount=10,
             frequency=self.plan_model.FREQ_MONTHLY,
         )
         self.customer_guid = self.customer_model.create(
             company_guid=self.company_guid,
         )
         self.subscription_guid = self.subscription_model.create(
             customer_guid=self.customer_guid,
             plan_guid=self.plan_guid,
             payment_uri='/v1/credit_card/tester',
         )
Example #11
0
    def test_create_subscription_to_other_company_customer(self):
        from billy.models.company import CompanyModel
        from billy.models.customer import CustomerModel

        company_model = CompanyModel(self.testapp.session)
        customer_model = CustomerModel(self.testapp.session)
        with db_transaction.manager:
            other_company_guid = company_model.create(processor_key="MOCK_PROCESSOR_KEY")
            other_customer_guid = customer_model.create(company_guid=other_company_guid)

        self.testapp.post(
            "/v1/subscriptions",
            dict(customer_guid=other_customer_guid, plan_guid=self.plan_guid),
            extra_environ=dict(REMOTE_USER=self.api_key),
            status=403,
        )
Example #12
0
    def test_server_info_with_transaction(self):
        from billy.models.company import CompanyModel
        from billy.models.customer import CustomerModel
        from billy.models.plan import PlanModel
        from billy.models.subscription import SubscriptionModel
        from billy.models.transaction import TransactionModel

        company_model = CompanyModel(self.testapp.session)
        customer_model = CustomerModel(self.testapp.session)
        plan_model = PlanModel(self.testapp.session)
        subscription_model = SubscriptionModel(self.testapp.session)
        transaction_model = TransactionModel(self.testapp.session)

        with db_transaction.manager:
            company_guid = company_model.create(
                processor_key='MOCK_PROCESSOR_KEY',
            )
            customer_guid = customer_model.create(
                company_guid=company_guid
            )
            plan_guid = plan_model.create(
                company_guid=company_guid,
                frequency=plan_model.FREQ_WEEKLY,
                plan_type=plan_model.TYPE_CHARGE,
                amount=10,
            )
            subscription_guid = subscription_model.create(
                customer_guid=customer_guid,
                plan_guid=plan_guid,
            )
            transaction_guid = transaction_model.create(
                subscription_guid=subscription_guid,
                transaction_type=transaction_model.TYPE_CHARGE,
                amount=10,
                payment_uri='/v1/cards/tester',
                scheduled_at=datetime.datetime.utcnow(),
            )

        transaction = transaction_model.get(transaction_guid)

        res = self.testapp.get('/', status=200)
        self.assertEqual(res.json['last_transaction_created_at'], 
                         transaction.created_at.isoformat())
Example #13
0
    def test_customer_list(self):
        from billy.models.customer import CustomerModel 
        customer_model = CustomerModel(self.testapp.session)
        with db_transaction.manager:
            guids = []
            for i in range(4):
                with freeze_time('2013-08-16 00:00:{:02}'.format(i + 1)):
                    guid = customer_model.create(self.company_guid)
                    guids.append(guid)
        guids = list(reversed(guids))

        res = self.testapp.get(
            '/v1/customers',
            extra_environ=dict(REMOTE_USER=self.api_key), 
            status=200,
        )
        items = res.json['items']
        result_guids = [item['guid'] for item in items]
        self.assertEqual(result_guids, guids)
Example #14
0
def customer_list_post(request):
    """Create a new customer 

    """
    company = auth_api_key(request)
    form = validate_form(CustomerCreateForm, request)
    
    external_id = form.data.get('external_id')
    company_guid = company.guid

    # TODO: make sure user cannot create a customer to a deleted company

    model = CustomerModel(request.session)
    # TODO: do validation here
    with db_transaction.manager:
        guid = model.create(
            external_id=external_id,
            company_guid=company_guid, 
        )
    customer = model.get(guid)
    return customer 
Example #15
0
 def test_get_transaction_of_other_company(self):
     from billy.models.company import CompanyModel
     from billy.models.customer import CustomerModel
     from billy.models.plan import PlanModel
     from billy.models.subscription import SubscriptionModel
     from billy.models.transaction import TransactionModel
     company_model = CompanyModel(self.testapp.session)
     customer_model = CustomerModel(self.testapp.session)
     plan_model = PlanModel(self.testapp.session)
     subscription_model = SubscriptionModel(self.testapp.session)
     transaction_model = TransactionModel(self.testapp.session)
     with db_transaction.manager:
         other_company_guid = company_model.create(
             processor_key='MOCK_PROCESSOR_KEY',
         )
         other_customer_guid = customer_model.create(
             company_guid=other_company_guid
         )
         other_plan_guid = plan_model.create(
             company_guid=other_company_guid,
             frequency=plan_model.FREQ_WEEKLY,
             plan_type=plan_model.TYPE_CHARGE,
             amount=10,
         )
         other_subscription_guid = subscription_model.create(
             customer_guid=other_customer_guid,
             plan_guid=other_plan_guid,
         )
         other_transaction_guid = transaction_model.create(
             subscription_guid=other_subscription_guid,
             transaction_type=transaction_model.TYPE_CHARGE,
             amount=10,
             payment_uri='/v1/cards/tester',
             scheduled_at=datetime.datetime.utcnow(),
         )
     self.testapp.get(
         '/v1/transactions/{}'.format(other_transaction_guid), 
         extra_environ=dict(REMOTE_USER=self.api_key), 
         status=403,
     )
Example #16
0
 def setUp(self):
     from pyramid.testing import DummyRequest
     from billy.models.company import CompanyModel
     from billy.models.customer import CustomerModel
     from billy.models.plan import PlanModel
     from billy.models.subscription import SubscriptionModel
     from billy.models.transaction import TransactionModel
     super(TestRenderer, self).setUp()
     company_model = CompanyModel(self.testapp.session)
     customer_model = CustomerModel(self.testapp.session)
     plan_model = PlanModel(self.testapp.session)
     subscription_model = SubscriptionModel(self.testapp.session)
     transaction_model = TransactionModel(self.testapp.session)
     with db_transaction.manager:
         self.company_guid = company_model.create(
             processor_key='MOCK_PROCESSOR_KEY',
         )
         self.customer_guid = customer_model.create(
             company_guid=self.company_guid
         )
         self.plan_guid = plan_model.create(
             company_guid=self.company_guid,
             frequency=plan_model.FREQ_WEEKLY,
             plan_type=plan_model.TYPE_CHARGE,
             amount=10,
         )
         self.subscription_guid = subscription_model.create(
             customer_guid=self.customer_guid,
             plan_guid=self.plan_guid,
         )
         self.transaction_guid = transaction_model.create(
             subscription_guid=self.subscription_guid,
             transaction_type=transaction_model.TYPE_CHARGE,
             amount=10,
             payment_uri='/v1/cards/tester',
             scheduled_at=datetime.datetime.utcnow(),
         )
     self.dummy_request = DummyRequest()
Example #17
0
class TestSubscriptionModel(ModelTestCase):

    def setUp(self):
        from billy.models.company import CompanyModel
        from billy.models.customer import CustomerModel
        from billy.models.plan import PlanModel
        super(TestSubscriptionModel, self).setUp()
        # build the basic scenario for plan model
        self.company_model = CompanyModel(self.session)
        self.customer_model = CustomerModel(self.session)
        self.plan_model = PlanModel(self.session)
        with db_transaction.manager:
            self.company_guid = self.company_model.create('my_secret_key')
            self.daily_plan_guid = self.plan_model.create(
                company_guid=self.company_guid,
                plan_type=self.plan_model.TYPE_CHARGE,
                amount=1000,  # 10 dollars
                frequency=self.plan_model.FREQ_DAILY,
            )
            self.weekly_plan_guid = self.plan_model.create(
                company_guid=self.company_guid,
                plan_type=self.plan_model.TYPE_CHARGE,
                amount=1000,
                frequency=self.plan_model.FREQ_WEEKLY,
            )
            self.monthly_plan_guid = self.plan_model.create(
                company_guid=self.company_guid,
                plan_type=self.plan_model.TYPE_CHARGE,
                amount=1000,
                frequency=self.plan_model.FREQ_MONTHLY,
            )
            self.customer_tom_guid = self.customer_model.create(
                company_guid=self.company_guid,
            )

    def make_one(self, *args, **kwargs):
        from billy.models.subscription import SubscriptionModel
        return SubscriptionModel(*args, **kwargs)

    def test_get_subscription(self):
        model = self.make_one(self.session)

        subscription = model.get('SU_NON_EXIST')
        self.assertEqual(subscription, None)

        with self.assertRaises(KeyError):
            model.get('SU_NON_EXIST', raise_error=True)

        with db_transaction.manager:
            guid = model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
            )

        subscription = model.get(guid, raise_error=True)
        self.assertEqual(subscription.guid, guid)

    def test_create(self):
        model = self.make_one(self.session)
        amount = 5566
        external_id = '5566_GOOD_BROTHERS'
        customer_guid = self.customer_tom_guid
        plan_guid = self.monthly_plan_guid
        payment_uri = '/v1/credit_cards/id'

        with db_transaction.manager:
            guid = model.create(
                customer_guid=customer_guid,
                plan_guid=plan_guid,
                amount=amount,
                external_id=external_id,
                payment_uri=payment_uri, 
            )

        now = datetime.datetime.utcnow()

        subscription = model.get(guid)
        self.assertEqual(subscription.guid, guid)
        self.assert_(subscription.guid.startswith('SU'))
        self.assertEqual(subscription.customer_guid, customer_guid)
        self.assertEqual(subscription.plan_guid, plan_guid)
        self.assertEqual(subscription.amount, amount)
        self.assertEqual(subscription.external_id, external_id)
        self.assertEqual(subscription.payment_uri, payment_uri)
        self.assertEqual(subscription.period, 0)
        self.assertEqual(subscription.canceled, False)
        self.assertEqual(subscription.canceled_at, None)
        self.assertEqual(subscription.started_at, now)
        self.assertEqual(subscription.next_transaction_at, now)
        self.assertEqual(subscription.created_at, now)
        self.assertEqual(subscription.updated_at, now)

    def test_create_different_created_updated_time(self):
        from billy.models import tables
        model = self.make_one(self.session)

        results = [
            datetime.datetime(2013, 8, 16, 1),
            datetime.datetime(2013, 8, 16, 2),
        ]

        def mock_utcnow():
            return results.pop(0)

        tables.set_now_func(mock_utcnow)

        with db_transaction.manager:
            guid = model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
            )

        subscription = model.get(guid)
        self.assertEqual(subscription.created_at, subscription.updated_at)

    def test_create_with_started_at(self):
        model = self.make_one(self.session)
        customer_guid = self.customer_tom_guid
        plan_guid = self.monthly_plan_guid
        started_at = datetime.datetime.utcnow() + datetime.timedelta(days=1)

        with db_transaction.manager:
            guid = model.create(
                customer_guid=customer_guid,
                plan_guid=plan_guid,
                started_at=started_at
            )

        subscription = model.get(guid)
        self.assertEqual(subscription.guid, guid)
        self.assertEqual(subscription.started_at, started_at)

    def test_create_with_past_started_at(self):
        model = self.make_one(self.session)
        started_at = datetime.datetime.utcnow() - datetime.timedelta(days=1)
        with self.assertRaises(ValueError):
            model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
                started_at=started_at
            )

    def test_create_with_bad_amount(self):
        model = self.make_one(self.session)

        with self.assertRaises(ValueError):
            model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
                amount=-0.1,
            )
        with self.assertRaises(ValueError):
            model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
                amount=0,
            )

    def test_update(self):
        model = self.make_one(self.session)

        with db_transaction.manager:
            guid = model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
                external_id='old external id'
            )

        subscription = model.get(guid)
        external_id = 'new external id'

        with db_transaction.manager:
            model.update(
                guid=guid,
                external_id=external_id,
            )

        subscription = model.get(guid)
        self.assertEqual(subscription.external_id, external_id)

    def test_update_updated_at(self):
        model = self.make_one(self.session)

        with db_transaction.manager:
            guid = model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
            )

        subscription = model.get(guid)
        created_at = subscription.created_at

        # advanced the current date time
        with freeze_time('2013-08-16 07:00:01'):
            with db_transaction.manager:
                model.update(guid=guid)
            updated_at = datetime.datetime.utcnow()

        subscription = model.get(guid)
        self.assertEqual(subscription.canceled_at, None)
        self.assertEqual(subscription.updated_at, updated_at)
        self.assertEqual(subscription.created_at, created_at)

        # advanced the current date time even more
        with freeze_time('2013-08-16 08:35:40'):
            # this should update the updated_at field only
            with db_transaction.manager:
                model.update(guid)
            updated_at = datetime.datetime.utcnow()

        subscription = model.get(guid)
        self.assertEqual(subscription.canceled_at, None)
        self.assertEqual(subscription.updated_at, updated_at)
        self.assertEqual(subscription.created_at, created_at)

    def test_update_with_wrong_args(self):
        model = self.make_one(self.session)
        with db_transaction.manager:
            guid = model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
            )
        # make sure passing wrong argument will raise error
        with self.assertRaises(TypeError):
            model.update(guid, wrong_arg=True, neme='john')

    def test_subscription_cancel(self):
        model = self.make_one(self.session)

        with db_transaction.manager:
            guid = model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
            )
            model.cancel(guid)

        now = datetime.datetime.utcnow()

        subscription = model.get(guid)
        self.assertEqual(subscription.canceled, True)
        self.assertEqual(subscription.canceled_at, now)

    def test_subscription_cancel_not_done_transactions(self):
        from billy.models.transaction import TransactionModel
        model = self.make_one(self.session)
        tx_model = TransactionModel(self.session)

        with db_transaction.manager:
            guid = model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
            )

        # okay, 08-16, 09-16, 10-16, 11-16, so we should have 4 new transactions
        # and we assume the transactions status as shown as following:
        # 
        #     [DONE, RETRYING, INIT, FAILED]
        #
        # when we cancel the subscription, the status should be
        # 
        #     [DONE, CANCELED, CANCELED, FAILED]
        #
        init_status = [
            tx_model.STATUS_DONE,
            tx_model.STATUS_RETRYING,
            tx_model.STATUS_INIT,
            tx_model.STATUS_FAILED,
        ]
        with freeze_time('2013-11-16'):
            with db_transaction.manager:
                tx_guids = model.yield_transactions()
                for tx_guid, status in zip(tx_guids, init_status):
                    transaction = tx_model.get(tx_guid)
                    transaction.status = status
                    self.session.add(transaction)
                self.session.add(transaction)
            with db_transaction.manager:
                model.cancel(guid)

        transactions = [tx_model.get(tx_guid) for tx_guid in tx_guids]
        status_list = [tx.status for tx in transactions]
        self.assertEqual(status_list, [
            tx_model.STATUS_DONE, 
            tx_model.STATUS_CANCELED,
            tx_model.STATUS_CANCELED,
            tx_model.STATUS_FAILED,
        ])

    def test_subscription_cancel_with_prorated_refund(self):
        from billy.models.transaction import TransactionModel
        model = self.make_one(self.session)
        tx_model = TransactionModel(self.session)

        with freeze_time('2013-06-01'):
            with db_transaction.manager:
                guid = model.create(
                    customer_guid=self.customer_tom_guid,
                    plan_guid=self.monthly_plan_guid,
                )
                tx_guids = model.yield_transactions()
                transaction = tx_model.get(tx_guids[0])
                transaction.status = tx_model.STATUS_DONE
                transaction.external_id = 'MOCK_BALANCED_DEBIT_URI'
                self.session.add(transaction)

        # it is a monthly plan, there is 30 days in June, and only
        # 6 days are elapsed, so 6 / 30 days, the rate should be 1 - 0.2 = 0.8
        # and we have 1000 cent as the amount, we should return 800 to customer
        with freeze_time('2013-06-07'):
            with db_transaction.manager:
                refund_guid = model.cancel(guid, prorated_refund=True)

        transaction = tx_model.get(refund_guid)
        self.assertEqual(transaction.refund_to_guid, tx_guids[0])
        self.assertEqual(transaction.subscription_guid, guid)
        self.assertEqual(transaction.transaction_type, tx_model.TYPE_REFUND)
        self.assertEqual(transaction.amount, 800)

    def test_subscription_cancel_with_wrong_arguments(self):
        from billy.models.transaction import TransactionModel
        model = self.make_one(self.session)
        tx_model = TransactionModel(self.session)

        with db_transaction.manager:
            guid = model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
            )
            tx_guids = model.yield_transactions()
            transaction = tx_model.get(tx_guids[0])
            transaction.status = tx_model.STATUS_DONE
            transaction.external_id = 'MOCK_BALANCED_DEBIT_URI'
            self.session.add(transaction)

        # we should not allow both prorated_refund and refund_amount to 
        # be set
        with self.assertRaises(ValueError):
            model.cancel(guid, prorated_refund=True, refund_amount=10)
        # we should not allow refunding amount that grather than original
        # subscription amount
        with self.assertRaises(ValueError):
            model.cancel(guid, refund_amount=1001)

    def test_subscription_cancel_with_refund_amount(self):
        from billy.models.transaction import TransactionModel
        model = self.make_one(self.session)
        tx_model = TransactionModel(self.session)

        with db_transaction.manager:
            guid = model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
            )
            tx_guids = model.yield_transactions()
            transaction = tx_model.get(tx_guids[0])
            transaction.status = tx_model.STATUS_DONE
            transaction.external_id = 'MOCK_BALANCED_DEBIT_URI'
            self.session.add(transaction)

        # let's cancel and refund the latest transaction with amount 566 cent
        with db_transaction.manager:
            refund_guid = model.cancel(guid, refund_amount=566)

        transaction = tx_model.get(refund_guid)
        self.assertEqual(transaction.refund_to_guid, tx_guids[0])
        self.assertEqual(transaction.subscription_guid, guid)
        self.assertEqual(transaction.transaction_type, tx_model.TYPE_REFUND)
        self.assertEqual(transaction.amount, 566)

    def test_subscription_cancel_with_prorated_refund_and_amount_overwrite(self):
        from billy.models.transaction import TransactionModel
        model = self.make_one(self.session)
        tx_model = TransactionModel(self.session)

        with freeze_time('2013-06-01'):
            with db_transaction.manager:
                guid = model.create(
                    customer_guid=self.customer_tom_guid,
                    plan_guid=self.monthly_plan_guid,
                    amount=10000,
                )
                tx_guids = model.yield_transactions()
                transaction = tx_model.get(tx_guids[0])
                transaction.status = tx_model.STATUS_DONE
                transaction.external_id = 'MOCK_BALANCED_DEBIT_URI'
                self.session.add(transaction)

        # it is a monthly plan, there is 30 days in June, and only
        # 6 days are elapsed, so 6 / 30 days, the rate should be 1 - 0.2 = 0.8
        # and we have 100 dollars as the amount, we should return 80 dollars to 
        # customer
        with freeze_time('2013-06-07'):
            with db_transaction.manager:
                refund_guid = model.cancel(guid, prorated_refund=True)

        transaction = tx_model.get(refund_guid)
        self.assertEqual(transaction.amount, 8000)

    def test_subscription_cancel_with_prorated_refund_rounding(self):
        from billy.models.transaction import TransactionModel
        model = self.make_one(self.session)
        tx_model = TransactionModel(self.session)

        with freeze_time('2013-06-01'):
            with db_transaction.manager:
                guid = model.create(
                    customer_guid=self.customer_tom_guid,
                    plan_guid=self.monthly_plan_guid,
                )
                tx_guids = model.yield_transactions()
                transaction = tx_model.get(tx_guids[0])
                transaction.status = tx_model.STATUS_DONE
                transaction.external_id = 'MOCK_BALANCED_DEBIT_URI'
                self.session.add(transaction)

        # 17 / 30 days, the rate should be 1 - 0.56666..., which is
        # 0.43333...
        with freeze_time('2013-06-18'):
            with db_transaction.manager:
                refund_guid = model.cancel(guid, prorated_refund=True)

        transaction = tx_model.get(refund_guid)
        self.assertEqual(transaction.amount, 433)

    def test_subscription_cancel_with_zero_refund(self):
        model = self.make_one(self.session)

        with freeze_time('2013-06-01'):
            with db_transaction.manager:
                guid = model.create(
                    customer_guid=self.customer_tom_guid,
                    plan_guid=self.monthly_plan_guid,
                )
                model.yield_transactions()
        # the subscription period is finished, nothing to refund
        with freeze_time('2013-07-01'):
            with db_transaction.manager:
                refund_guid = model.cancel(guid, prorated_refund=True)

        self.assertEqual(refund_guid, None)
        subscription = model.get(guid)
        transactions = subscription.transactions
        self.assertEqual(len(transactions), 1)

    def test_subscription_cancel_twice(self):
        from billy.models.subscription import SubscriptionCanceledError
        model = self.make_one(self.session)

        with db_transaction.manager:
            guid = model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
            )
            model.cancel(guid)

        with self.assertRaises(SubscriptionCanceledError):
            model.cancel(guid)

    def test_yield_transactions(self):
        from billy.models.transaction import TransactionModel

        model = self.make_one(self.session)
        tx_model = TransactionModel(self.session)

        now = datetime.datetime.utcnow()

        with db_transaction.manager:
            guid = model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
            )
            tx_guids = model.yield_transactions()

        self.assertEqual(len(tx_guids), 1)

        subscription = model.get(guid)
        transactions = subscription.transactions
        self.assertEqual(len(transactions), 1)

        transaction = transactions[0]
        self.assertEqual(transaction.guid, tx_guids[0])
        self.assertEqual(transaction.subscription_guid, guid)
        self.assertEqual(transaction.amount, subscription.plan.amount)
        self.assertEqual(transaction.transaction_type, 
                         TransactionModel.TYPE_CHARGE)
        self.assertEqual(transaction.scheduled_at, now)
        self.assertEqual(transaction.created_at, now)
        self.assertEqual(transaction.updated_at, now)
        self.assertEqual(transaction.status, TransactionModel.STATUS_INIT)

        # we should not yield new transaction as the datetime is the same
        with db_transaction.manager:
            tx_guids = model.yield_transactions()
        self.assertFalse(tx_guids)
        subscription = model.get(guid)
        self.assertEqual(len(subscription.transactions), 1)

        # should not yield new transaction as 09-16 is the date
        with freeze_time('2013-09-15'):
            with db_transaction.manager:
                tx_guids = model.yield_transactions()
        self.assertFalse(tx_guids)
        subscription = model.get(guid)
        self.assertEqual(len(subscription.transactions), 1)

        # okay, should yield new transaction now
        with freeze_time('2013-09-16'):
            with db_transaction.manager:
                tx_guids = model.yield_transactions()
            scheduled_at = datetime.datetime.utcnow()
        self.assertEqual(len(tx_guids), 1)
        subscription = model.get(guid)
        self.assertEqual(len(subscription.transactions), 2)

        transaction = tx_model.get(tx_guids[0])
        self.assertEqual(transaction.subscription_guid, guid)
        self.assertEqual(transaction.amount, subscription.plan.amount)
        self.assertEqual(transaction.transaction_type, 
                         TransactionModel.TYPE_CHARGE)
        self.assertEqual(transaction.scheduled_at, scheduled_at)
        self.assertEqual(transaction.created_at, scheduled_at)
        self.assertEqual(transaction.updated_at, scheduled_at)
        self.assertEqual(transaction.status, TransactionModel.STATUS_INIT)

    def test_yield_transactions_for_specific_subscriptions(self):
        from billy.models.transaction import TransactionModel

        model = self.make_one(self.session)
        tx_model = TransactionModel(self.session)

        with db_transaction.manager:
            guid1 = model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
            )
            model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
            )
            guid2 = model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
            )
            model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
            )
            tx_guids = model.yield_transactions([guid1, guid2])

        self.assertEqual(len(tx_guids), 2)
        subscription_guids = [tx_model.get(tx_guid).subscription_guid 
                              for tx_guid in tx_guids]
        self.assertEqual(set(subscription_guids), set([guid1, guid2]))

    def test_yield_transactions_with_multiple_period(self):
        model = self.make_one(self.session)

        with db_transaction.manager:
            guid = model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
            )

        # okay, 08-16, 09-16, 10-16, so we should have 3 new transactions
        with freeze_time('2013-10-16'):
            with db_transaction.manager:
                tx_guids = model.yield_transactions()

        self.assertEqual(len(set(tx_guids)), 3)
        subscription = model.get(guid)
        self.assertEqual(len(subscription.transactions), 3)

        sub_tx_guids = [tx.guid for tx in subscription.transactions]
        self.assertEqual(set(tx_guids), set(sub_tx_guids))

        tx_dates = [tx.scheduled_at for tx in subscription.transactions]
        self.assertEqual(set(tx_dates), set([
            datetime.datetime(2013, 8, 16),
            datetime.datetime(2013, 9, 16),
            datetime.datetime(2013, 10, 16),
        ]))

    def test_yield_transactions_with_amount_overwrite(self):
        model = self.make_one(self.session)

        with db_transaction.manager:
            guid = model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
                amount=5566, 
            )

        # okay, 08-16, 09-16, 10-16, so we should have 3 new transactions
        with freeze_time('2013-10-16'):
            with db_transaction.manager:
                model.yield_transactions()

        subscription = model.get(guid)
        amounts = [tx.amount for tx in subscription.transactions]
        self.assertEqual(amounts, [
            5566,
            5566,
            5566,
        ])

    def test_yield_transactions_with_multiple_interval(self):
        model = self.make_one(self.session)

        with db_transaction.manager:
            plan_guid = self.plan_model.create(
                company_guid=self.company_guid,
                plan_type=self.plan_model.TYPE_PAYOUT,
                amount=10,
                frequency=self.plan_model.FREQ_MONTHLY,
                interval=2,
            )
            guid = model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=plan_guid,
            )

        # okay, 08-16, 10-16, so we should have 2 new transactions
        with freeze_time('2013-10-16'):
            with db_transaction.manager:
                tx_guids = model.yield_transactions()

        self.assertEqual(len(set(tx_guids)), 2)
        subscription = model.get(guid)
        self.assertEqual(len(subscription.transactions), 2)

    def test_yield_transactions_with_payout(self):
        from billy.models.transaction import TransactionModel
        model = self.make_one(self.session)

        with db_transaction.manager:
            plan_guid = self.plan_model.create(
                company_guid=self.company_guid,
                plan_type=self.plan_model.TYPE_PAYOUT,
                amount=10,
                frequency=self.plan_model.FREQ_MONTHLY,
            )
            guid = model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=plan_guid,
            )
            model.yield_transactions()

        subscription = model.get(guid)
        transaction = subscription.transactions[0]
        self.assertEqual(transaction.transaction_type, 
                         TransactionModel.TYPE_PAYOUT)

    def test_yield_transactions_with_started_at(self):
        model = self.make_one(self.session)

        with db_transaction.manager:
            guid = model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
                started_at=datetime.datetime(2013, 9, 1),
            )

        with db_transaction.manager:
            tx_guids = model.yield_transactions()

        self.assertFalse(tx_guids)
        subscription = model.get(guid)
        self.assertFalse(subscription.transactions)

        # 
        with freeze_time('2013-09-01'):
            with db_transaction.manager:
                tx_guids = model.yield_transactions()

        self.assertEqual(len(set(tx_guids)), 1)
        subscription = model.get(guid)
        self.assertEqual(len(subscription.transactions), 1)

        transaction = subscription.transactions[0]
        self.assertEqual(transaction.scheduled_at, 
                         datetime.datetime(2013, 9, 1))

    def test_yield_transactions_with_wrong_type(self):
        model = self.make_one(self.session)

        with db_transaction.manager:
            guid = model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
            )
            subscription = model.get(guid)
            subscription.plan.plan_type = 999
            self.session.add(subscription.plan)

        with self.assertRaises(ValueError):
            model.yield_transactions()

    def test_yield_transactions_with_canceled_subscription(self):
        model = self.make_one(self.session)

        with db_transaction.manager:
            guid = model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
                started_at=datetime.datetime(2013, 9, 1),
            )
            model.cancel(guid)

        with db_transaction.manager:
            tx_guids = model.yield_transactions()

        self.assertFalse(tx_guids)
        subscription = model.get(guid)
        self.assertFalse(subscription.transactions)

    def test_yield_transactions_with_canceled_in_middle(self):
        model = self.make_one(self.session)

        with db_transaction.manager:
            guid = model.create(
                customer_guid=self.customer_tom_guid,
                plan_guid=self.monthly_plan_guid,
            )

        # 08-16, 09-16, 10-16 transactions should be yielded
        with freeze_time('2013-10-16'):
            with db_transaction.manager:
                tx_guids = model.yield_transactions()

        self.assertEqual(len(set(tx_guids)), 3)
        subscription = model.get(guid)
        self.assertEqual(len(subscription.transactions), 3)

        # okay, cancel this, there should be no more new transactions
        with db_transaction.manager:
            model.cancel(guid)

        with freeze_time('2020-12-31'):
            with db_transaction.manager:
                tx_guids = model.yield_transactions()

        self.assertFalse(tx_guids)
        subscription = model.get(guid)
        self.assertEqual(len(subscription.transactions), 3)

    def test_list_by_company_guid(self):
        model = self.make_one(self.session)

        # create another company with subscriptions
        with db_transaction.manager:
            other_company_guid = self.company_model.create('my_secret_key')
            other_plan_guid = self.plan_model.create(
                company_guid=other_company_guid,
                plan_type=self.plan_model.TYPE_CHARGE,
                amount=10,
                frequency=self.plan_model.FREQ_MONTHLY,
            )
            other_customer_guid = self.customer_model.create(
                company_guid=other_company_guid,
            )
            guids1 = []
            for i in range(2):
                with freeze_time('2013-08-16 00:00:{:02}'.format(i)):
                    guid = model.create(
                        customer_guid=other_customer_guid,
                        plan_guid=other_plan_guid,
                    )
                    guids1.append(guid)
        with db_transaction.manager:
            guids2 = []
            for i in range(3):
                with freeze_time('2013-08-16 00:00:{:02}'.format(i)):
                    guid = model.create(
                        customer_guid=self.customer_tom_guid,
                        plan_guid=self.monthly_plan_guid,
                    )
                    guids2.append(guid)

        guids1 = list(reversed(guids1))
        guids2 = list(reversed(guids2))

        def assert_list_by_company_guid(
            company_guid, 
            expected, 
            offset=None, 
            limit=None,
        ):
            result = model.list_by_company_guid(
                company_guid, 
                offset=offset, 
                limit=limit,
            )
            result_guids = [s.guid for s in result]
            self.assertEqual(result_guids, expected)

        assert_list_by_company_guid(other_company_guid, guids1)
        assert_list_by_company_guid(other_company_guid, guids1[1:], offset=1)
        assert_list_by_company_guid(other_company_guid, guids1[2:], offset=2)
        assert_list_by_company_guid(other_company_guid, guids1[:1], limit=1)
        assert_list_by_company_guid(self.company_guid, guids2)
    def test_main_with_crash(self):
        from pyramid.paster import get_appsettings
        from billy.models import setup_database
        from billy.models.company import CompanyModel
        from billy.models.customer import CustomerModel
        from billy.models.plan import PlanModel
        from billy.models.subscription import SubscriptionModel
        from billy.scripts import initializedb
        from billy.scripts import process_transactions

        class MockProcessor(object):

            def __init__(self):
                self.charges = {}
                self.tx_sn = 0
                self.called_times = 0

            def create_customer(self, customer):
                return 'MOCK_PROCESSOR_CUSTOMER_ID'

            def prepare_customer(self, customer, payment_uri=None):
                pass

            def charge(self, transaction):
                self.called_times += 1
                if self.called_times == 2:
                    raise KeyboardInterrupt
                guid = transaction.guid
                if guid in self.charges:
                    return self.charges[guid]
                self.charges[guid] = self.tx_sn
                self.tx_sn += 1

        mock_processor = MockProcessor()
        
        cfg_path = os.path.join(self.temp_dir, 'config.ini')
        with open(cfg_path, 'wt') as f:
            f.write(textwrap.dedent("""\
            [app:main]
            use = egg:billy

            sqlalchemy.url = sqlite:///%(here)s/billy.sqlite
            """))
        initializedb.main([initializedb.__file__, cfg_path])

        settings = get_appsettings(cfg_path)
        settings = setup_database({}, **settings)
        session = settings['session']
        company_model = CompanyModel(session)
        customer_model = CustomerModel(session)
        plan_model = PlanModel(session)
        subscription_model = SubscriptionModel(session)

        with db_transaction.manager:
            company_guid = company_model.create('my_secret_key')
            plan_guid = plan_model.create(
                company_guid=company_guid,
                plan_type=plan_model.TYPE_CHARGE,
                amount=10,
                frequency=plan_model.FREQ_MONTHLY,
            )
            customer_guid = customer_model.create(
                company_guid=company_guid,
            )
            subscription_model.create(
                customer_guid=customer_guid,
                plan_guid=plan_guid,
                payment_uri='/v1/cards/tester',
            )
            subscription_model.create(
                customer_guid=customer_guid,
                plan_guid=plan_guid,
                payment_uri='/v1/cards/tester',
            )

        with self.assertRaises(KeyboardInterrupt):
            process_transactions.main([process_transactions.__file__, cfg_path], 
                                      processor=mock_processor)

        process_transactions.main([process_transactions.__file__, cfg_path], 
                                  processor=mock_processor)

        # here is the story, we have two subscriptions here
        #   
        #   Subscription1
        #   Subscription2
        #
        # And the time is not advanced, so we should only have two transactions
        # to be yielded and processed. However, we assume bad thing happens
        # durring the process. We let the second call to charge of processor 
        # raises a KeyboardInterrupt error. So, it would look like this
        #
        #   charge for transaction from Subscription1
        #   charge for transaction from Subscription2 (Crash)
        #
        # Then, we perform the process_transactions again, if it works 
        # correctly, the first transaction is already yield and processed. 
        # 
        #   charge for transaction from Subscription2
        #
        # So, there would only be two charges in processor. This is mainly
        # for making sure we won't duplicate charges/payouts
        self.assertEqual(len(mock_processor.charges), 2)
Example #19
0
class TestBalancedProcessorModel(ModelTestCase):

    def setUp(self):
        from billy.models.company import CompanyModel
        from billy.models.customer import CustomerModel
        from billy.models.plan import PlanModel
        from billy.models.subscription import SubscriptionModel
        from billy.models.transaction import TransactionModel
        super(TestBalancedProcessorModel, self).setUp()
        # build the basic scenario for transaction model
        self.company_model = CompanyModel(self.session)
        self.customer_model = CustomerModel(self.session)
        self.plan_model = PlanModel(self.session)
        self.subscription_model = SubscriptionModel(self.session)
        self.transaction_model = TransactionModel(self.session)
        with db_transaction.manager:
            self.company_guid = self.company_model.create('my_secret_key')
            self.plan_guid = self.plan_model.create(
                company_guid=self.company_guid,
                plan_type=self.plan_model.TYPE_CHARGE,
                amount=10,
                frequency=self.plan_model.FREQ_MONTHLY,
            )
            self.customer_guid = self.customer_model.create(
                company_guid=self.company_guid,
            )
            self.subscription_guid = self.subscription_model.create(
                customer_guid=self.customer_guid,
                plan_guid=self.plan_guid,
                payment_uri='/v1/credit_card/tester',
            )

    def make_one(self, *args, **kwargs):
        from billy.models.processors.balanced_payments import BalancedProcessor
        return BalancedProcessor(*args, **kwargs)

    def test_create_customer(self):
        import balanced

        customer = self.customer_model.get(self.customer_guid)

        # make sure API key is set correctly
        (
            flexmock(balanced)
            .should_receive('configure')
            .with_args('my_secret_key')
            .once()
        )

        # mock balanced customer instance
        mock_balanced_customer = (
            flexmock(uri='MOCK_BALANCED_CUSTOMER_URI')
            .should_receive('save')
            .replace_with(lambda: mock_balanced_customer)
            .once()
            .mock()
        )

        class BalancedCustomer(object):
            pass
        flexmock(BalancedCustomer).new_instances(mock_balanced_customer) 

        processor = self.make_one(customer_cls=BalancedCustomer)
        customer_id = processor.create_customer(customer)
        self.assertEqual(customer_id, 'MOCK_BALANCED_CUSTOMER_URI')

    def test_prepare_customer_with_card(self):
        import balanced

        with db_transaction.manager:
            self.customer_model.update(
                guid=self.customer_guid,
                external_id='MOCK_BALANCED_CUSTOMER_URI',
            )
        customer = self.customer_model.get(self.customer_guid)

        # make sure API key is set correctly
        (
            flexmock(balanced)
            .should_receive('configure')
            .with_args('my_secret_key')
            .once()
        )

        # mock balanced.Customer instance
        mock_balanced_customer = (
            flexmock()
            .should_receive('add_card')
            .with_args('/v1/cards/my_card')
            .once()
            .mock()
        )

        # mock balanced.Customer class
        class BalancedCustomer(object): 
            def find(self, uri):
                pass
        (
            flexmock(BalancedCustomer)
            .should_receive('find')
            .with_args('MOCK_BALANCED_CUSTOMER_URI')
            .replace_with(lambda _: mock_balanced_customer)
            .once()
        )

        processor = self.make_one(customer_cls=BalancedCustomer)
        processor.prepare_customer(customer, '/v1/cards/my_card')

    def test_prepare_customer_with_bank_account(self):
        import balanced

        with db_transaction.manager:
            self.customer_model.update(
                guid=self.customer_guid,
                external_id='MOCK_BALANCED_CUSTOMER_URI',
            )
        customer = self.customer_model.get(self.customer_guid)

        # make sure API key is set correctly
        (
            flexmock(balanced)
            .should_receive('configure')
            .with_args('my_secret_key')
            .once()
        )

        # mock balanced.Customer instance
        mock_balanced_customer = (
            flexmock()
            .should_receive('add_bank_account')
            .with_args('/v1/bank_accounts/my_account')
            .once()
            .mock()
        )

        # mock balanced.Customer class
        class BalancedCustomer(object): 
            def find(self, uri):
                pass
        (
            flexmock(BalancedCustomer)
            .should_receive('find')
            .with_args('MOCK_BALANCED_CUSTOMER_URI')
            .replace_with(lambda _: mock_balanced_customer)
            .once()
        )

        processor = self.make_one(customer_cls=BalancedCustomer)
        processor.prepare_customer(customer, '/v1/bank_accounts/my_account')

    def test_prepare_customer_with_none_payment_uri(self):
        with db_transaction.manager:
            self.customer_model.update(
                guid=self.customer_guid,
                external_id='MOCK_BALANCED_CUSTOMER_URI',
            )
        customer = self.customer_model.get(self.customer_guid)

        # mock balanced.Customer instance
        mock_balanced_customer = (
            flexmock()
            .should_receive('add_bank_account')
            .never()
            .mock()
        )

        # mock balanced.Customer class
        class BalancedCustomer(object): 
            def find(self, uri):
                pass
        (
            flexmock(BalancedCustomer)
            .should_receive('find')
            .with_args('MOCK_BALANCED_CUSTOMER_URI')
            .replace_with(lambda _: mock_balanced_customer)
            .never()
        )

        processor = self.make_one(customer_cls=BalancedCustomer)
        processor.prepare_customer(customer, None)

    def test_prepare_customer_with_bad_payment_uri(self):
        with db_transaction.manager:
            self.customer_model.update(
                guid=self.customer_guid,
                external_id='MOCK_BALANCED_CUSTOMER_URI',
            )
        customer = self.customer_model.get(self.customer_guid)

        # mock balanced.Customer instance
        mock_balanced_customer = flexmock()

        # mock balanced.Customer class
        class BalancedCustomer(object): 
            def find(self, uri):
                pass
        (
            flexmock(BalancedCustomer)
            .should_receive('find')
            .with_args('MOCK_BALANCED_CUSTOMER_URI')
            .replace_with(lambda _: mock_balanced_customer)
            .once()
        )

        processor = self.make_one(customer_cls=BalancedCustomer)
        with self.assertRaises(ValueError):
            processor.prepare_customer(customer, '/v1/bitcoin/12345')

    def _test_operation(
        self, 
        cls_name, 
        processor_method_name, 
        api_method_name,
        extra_api_kwargs,
    ):
        import balanced

        tx_model = self.transaction_model
        with db_transaction.manager:
            guid = tx_model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=tx_model.TYPE_CHARGE,
                amount=10,
                payment_uri='/v1/credit_card/tester',
                scheduled_at=datetime.datetime.utcnow(),
            )
            transaction = tx_model.get(guid)
            self.customer_model.update(
                guid=transaction.subscription.customer_guid,
                external_id='MOCK_BALANCED_CUSTOMER_URI',
            )
        transaction = tx_model.get(guid)

        # make sure API key is set correctly
        (
            flexmock(balanced)
            .should_receive('configure')
            .with_args('my_secret_key')
            .once()
        )

        # mock result page object of balanced.RESOURCE.query.filter(...)

        def mock_one():
            raise balanced.exc.NoResultFound

        mock_page = (
            flexmock()
            .should_receive('one')
            .replace_with(mock_one)
            .once()
            .mock()
        )

        # mock balanced.RESOURCE.query
        mock_query = (
            flexmock()
            .should_receive('filter')
            .with_args(**{'meta.billy.transaction_guid': transaction.guid})
            .replace_with(lambda **kw: mock_page)
            .mock()
        )

        # mock balanced.RESOURCE class
        class Resource(object): 
            pass
        Resource.query = mock_query

        # mock balanced.RESOURCE instance
        mock_resource = flexmock(uri='MOCK_BALANCED_RESOURCE_URI')

        # mock balanced.Customer instance
        kwargs = dict(
            amount=transaction.amount,
            meta={'billy.transaction_guid': transaction.guid},
            description=(
                'Generated by Billy from subscription {}, scheduled_at={}'
                .format(transaction.subscription.guid, transaction.scheduled_at)
            )
        )
        kwargs.update(extra_api_kwargs)
        mock_balanced_customer = (
            flexmock()
            .should_receive(api_method_name)
            .with_args(**kwargs)
            .replace_with(lambda **kw: mock_resource)
            .once()
            .mock()
        )

        # mock balanced.Customer class
        class BalancedCustomer(object): 
            def find(self, uri):
                pass
        (
            flexmock(BalancedCustomer)
            .should_receive('find')
            .with_args('MOCK_BALANCED_CUSTOMER_URI')
            .replace_with(lambda _: mock_balanced_customer)
            .once()
        )

        processor = self.make_one(
            customer_cls=BalancedCustomer, 
            **{cls_name: Resource}
        )
        method = getattr(processor, processor_method_name)
        balanced_tx_id = method(transaction)
        self.assertEqual(balanced_tx_id, 'MOCK_BALANCED_RESOURCE_URI')

    def _test_operation_with_created_record(
        self, 
        cls_name, 
        processor_method_name,
    ):
        tx_model = self.transaction_model
        with db_transaction.manager:
            guid = tx_model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=tx_model.TYPE_CHARGE,
                amount=10,
                payment_uri='/v1/credit_card/tester',
                scheduled_at=datetime.datetime.utcnow(),
            )
            transaction = tx_model.get(guid)
        transaction = tx_model.get(guid)

        # mock balanced.RESOURCE instance
        mock_resource = flexmock(uri='MOCK_BALANCED_RESOURCE_URI')

        # mock result page object of balanced.RESOURCE.query.filter(...)
        mock_page = (
            flexmock()
            .should_receive('one')
            .replace_with(lambda: mock_resource)
            .once()
            .mock()
        )

        # mock balanced.RESOURCE.query
        mock_query = (
            flexmock()
            .should_receive('filter')
            .with_args(**{'meta.billy.transaction_guid': transaction.guid})
            .replace_with(lambda **kw: mock_page)
            .mock()
        )

        # mock balanced.RESOURCE class
        class Resource(object): 
            pass
        Resource.query = mock_query

        processor = self.make_one(**{cls_name: Resource})
        method = getattr(processor, processor_method_name)
        balanced_res_uri = method(transaction)
        self.assertEqual(balanced_res_uri, 'MOCK_BALANCED_RESOURCE_URI')

    def test_charge(self):
        self._test_operation(
            cls_name='debit_cls', 
            processor_method_name='charge',
            api_method_name='debit',
            extra_api_kwargs=dict(source_uri='/v1/credit_card/tester'),
        )

    def test_charge_with_created_record(self):
        self._test_operation_with_created_record(
            cls_name='debit_cls',
            processor_method_name='charge',
        )

    def test_payout(self):
        self._test_operation(
            cls_name='credit_cls', 
            processor_method_name='payout',
            api_method_name='credit',
            extra_api_kwargs=dict(destination_uri='/v1/credit_card/tester'),
        )

    def test_payout_with_created_record(self):
        self._test_operation_with_created_record(
            cls_name='credit_cls',
            processor_method_name='payout',
        )

    def test_refund(self):
        import balanced

        tx_model = self.transaction_model
        with db_transaction.manager:
            charge_guid = tx_model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=tx_model.TYPE_CHARGE,
                amount=100,
                payment_uri='/v1/credit_card/tester',
                scheduled_at=datetime.datetime.utcnow(),
            )
            charge_transaction = tx_model.get(charge_guid)
            charge_transaction.status = tx_model.STATUS_DONE
            charge_transaction.external_id = 'MOCK_BALANCED_DEBIT_URI'
            self.session.add(charge_transaction)
            self.session.flush()

            refund_guid = tx_model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=tx_model.TYPE_REFUND,
                refund_to_guid=charge_guid,
                amount=56,
                scheduled_at=datetime.datetime.utcnow(),
            )

        transaction = tx_model.get(refund_guid)

        # make sure API key is set correctly
        (
            flexmock(balanced)
            .should_receive('configure')
            .with_args('my_secret_key')
            .once()
        )

        # mock result page object of balanced.Refund.query.filter(...)

        def mock_one():
            raise balanced.exc.NoResultFound

        mock_page = (
            flexmock()
            .should_receive('one')
            .replace_with(mock_one)
            .once()
            .mock()
        )

        # mock balanced.Refund.query
        mock_query = (
            flexmock()
            .should_receive('filter')
            .with_args(**{'meta.billy.transaction_guid': transaction.guid})
            .replace_with(lambda **kw: mock_page)
            .mock()
        )

        # mock balanced.Refund class
        class Refund(object): 
            pass
        Refund.query = mock_query

        # mock balanced.Refund instance
        mock_refund = flexmock(uri='MOCK_BALANCED_REFUND_URI')

        # mock balanced.Debit instance
        kwargs = dict(
            amount=transaction.amount,
            meta={'billy.transaction_guid': transaction.guid},
            description=(
                'Generated by Billy from subscription {}, scheduled_at={}'
                .format(transaction.subscription.guid, transaction.scheduled_at)
            )
        )
        mock_balanced_debit = (
            flexmock()
            .should_receive('refund')
            .with_args(**kwargs)
            .replace_with(lambda **kw: mock_refund)
            .once()
            .mock()
        )

        # mock balanced.Debit class
        class BalancedDebit(object): 
            def find(self, uri):
                pass
        (
            flexmock(BalancedDebit)
            .should_receive('find')
            .with_args('MOCK_BALANCED_DEBIT_URI')
            .replace_with(lambda _: mock_balanced_debit)
            .once()
        )

        processor = self.make_one(
            refund_cls=Refund,
            debit_cls=BalancedDebit,
        )
        refund_uri = processor.refund(transaction)
        self.assertEqual(refund_uri, 'MOCK_BALANCED_REFUND_URI')

    def test_refund_with_created_record(self):
        tx_model = self.transaction_model
        with db_transaction.manager:
            charge_guid = tx_model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=tx_model.TYPE_CHARGE,
                amount=100,
                payment_uri='/v1/credit_card/tester',
                scheduled_at=datetime.datetime.utcnow(),
            )
            charge_transaction = tx_model.get(charge_guid)
            charge_transaction.status = tx_model.STATUS_DONE
            charge_transaction.external_id = 'MOCK_BALANCED_DEBIT_URI'
            self.session.add(charge_transaction)
            self.session.flush()

            refund_guid = tx_model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=tx_model.TYPE_REFUND,
                refund_to_guid=charge_guid,
                amount=56,
                scheduled_at=datetime.datetime.utcnow(),
            )

        transaction = tx_model.get(refund_guid)

        # mock balanced.Refund instance
        mock_refund = flexmock(uri='MOCK_BALANCED_REFUND_URI')

        # mock result page object of balanced.Refund.query.filter(...)

        def mock_one():
            return mock_refund

        mock_page = (
            flexmock()
            .should_receive('one')
            .replace_with(mock_one)
            .once()
            .mock()
        )

        # mock balanced.Refund.query
        mock_query = (
            flexmock()
            .should_receive('filter')
            .with_args(**{'meta.billy.transaction_guid': transaction.guid})
            .replace_with(lambda **kw: mock_page)
            .mock()
        )

        # mock balanced.Refund class
        class Refund(object): 
            pass
        Refund.query = mock_query

        processor = self.make_one(refund_cls=Refund)
        refund_uri = processor.refund(transaction)
        self.assertEqual(refund_uri, 'MOCK_BALANCED_REFUND_URI')
Example #20
0
class TestTransactionModel(ModelTestCase):

    def setUp(self):
        from billy.models.company import CompanyModel
        from billy.models.customer import CustomerModel
        from billy.models.plan import PlanModel
        from billy.models.subscription import SubscriptionModel
        super(TestTransactionModel, self).setUp()
        # build the basic scenario for transaction model
        self.company_model = CompanyModel(self.session)
        self.customer_model = CustomerModel(self.session)
        self.plan_model = PlanModel(self.session)
        self.subscription_model = SubscriptionModel(self.session)
        with db_transaction.manager:
            self.company_guid = self.company_model.create('my_secret_key')
            self.plan_guid = self.plan_model.create(
                company_guid=self.company_guid,
                plan_type=self.plan_model.TYPE_CHARGE,
                amount=10,
                frequency=self.plan_model.FREQ_MONTHLY,
            )
            self.customer_guid = self.customer_model.create(
                company_guid=self.company_guid,
            )
            self.subscription_guid = self.subscription_model.create(
                customer_guid=self.customer_guid,
                plan_guid=self.plan_guid,
                payment_uri='/v1/cards/tester',
            )

    def make_one(self, *args, **kwargs):
        from billy.models.transaction import TransactionModel
        return TransactionModel(*args, **kwargs)

    def test_get_transaction(self):
        model = self.make_one(self.session)

        transaction = model.get('TX_NON_EXIST')
        self.assertEqual(transaction, None)

        with self.assertRaises(KeyError):
            model.get('TX_NON_EXIST', raise_error=True)

        with db_transaction.manager:
            guid = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_CHARGE,
                amount=10,
                payment_uri='/v1/cards/tester',
                scheduled_at=datetime.datetime.utcnow(),
            )

        transaction = model.get(guid, raise_error=True)
        self.assertEqual(transaction.guid, guid)

    def test_list_by_company_guid(self):
        model = self.make_one(self.session)
        # Following code basically crerates another company with records 
        # like this:
        #
        #     + Company (other)
        #         + Customer1 (shared by two subscriptions)
        #         + Plan1
        #             + Subscription1
        #                 + Transaction1
        #         + Plan2
        #             + Subscription2
        #                 + Transaction2
        #
        with db_transaction.manager:
            other_company_guid = self.company_model.create('my_secret_key')
            other_plan_guid1 = self.plan_model.create(
                company_guid=other_company_guid,
                plan_type=self.plan_model.TYPE_CHARGE,
                amount=10,
                frequency=self.plan_model.FREQ_MONTHLY,
            )
            other_plan_guid2 = self.plan_model.create(
                company_guid=other_company_guid,
                plan_type=self.plan_model.TYPE_CHARGE,
                amount=10,
                frequency=self.plan_model.FREQ_MONTHLY,
            )
            other_customer_guid = self.customer_model.create(
                company_guid=other_company_guid,
            )
            other_subscription_guid1 = self.subscription_model.create(
                customer_guid=other_customer_guid,
                plan_guid=other_plan_guid1,
                payment_uri='/v1/cards/tester',
            )
            other_subscription_guid2 = self.subscription_model.create(
                customer_guid=other_customer_guid,
                plan_guid=other_plan_guid2,
                payment_uri='/v1/cards/tester',
            )
        with db_transaction.manager:
            other_guid1 = model.create(
                subscription_guid=other_subscription_guid1,
                transaction_type=model.TYPE_CHARGE,
                amount=10,
                payment_uri='/v1/cards/tester',
                scheduled_at=datetime.datetime.utcnow(),
            )
            other_guid2 = model.create(
                subscription_guid=other_subscription_guid2,
                transaction_type=model.TYPE_CHARGE,
                amount=10,
                payment_uri='/v1/cards/tester',
                scheduled_at=datetime.datetime.utcnow(),
            )
        # Following code basically crerates our records under default company
        # like this:
        #
        #     + Company (default)
        #         + Customer1
        #         + Plan1
        #             + Subscription1
        #                 + Transaction1
        #                 + Transaction2
        #                 + Transaction3
        #
        with db_transaction.manager:
            guid1 = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_CHARGE,
                amount=10,
                payment_uri='/v1/cards/tester',
                scheduled_at=datetime.datetime.utcnow(),
            )
            guid2 = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_CHARGE,
                amount=10,
                payment_uri='/v1/cards/tester',
                scheduled_at=datetime.datetime.utcnow(),
            )
            guid3 = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_CHARGE,
                amount=10,
                payment_uri='/v1/cards/tester',
                scheduled_at=datetime.datetime.utcnow(),
            )
        result_guids = [tx.guid for tx in 
                        model.list_by_company_guid(self.company_guid)]
        self.assertEqual(set(result_guids), set([guid1, guid2, guid3]))
        result_guids = [tx.guid for tx in 
                        model.list_by_company_guid(other_company_guid)]
        self.assertEqual(set(result_guids), set([other_guid1, other_guid2]))

    def test_list_by_subscription_guid(self):
        model = self.make_one(self.session)
        # Following code basically crerates records like this:
        #
        #     + Subscription1
        #         + Transaction1
        #         + Transaction2
        #         + Transaction3
        #     + Subscription2
        #         + Transaction4
        #         + Transaction5
        #
        with db_transaction.manager:
            subscription_guid1 = self.subscription_model.create(
                customer_guid=self.customer_guid,
                plan_guid=self.plan_guid,
                payment_uri='/v1/cards/tester',
            )
            guid_ids1 = []
            for _ in range(3):
                guid = model.create(
                    subscription_guid=subscription_guid1,
                    transaction_type=model.TYPE_CHARGE,
                    amount=10,
                    payment_uri='/v1/cards/tester',
                    scheduled_at=datetime.datetime.utcnow(),
                )
                guid_ids1.append(guid)

            subscription_guid2 = self.subscription_model.create(
                customer_guid=self.customer_guid,
                plan_guid=self.plan_guid,
                payment_uri='/v1/cards/tester',
            )
            guid_ids2 = []
            for _ in range(2):
                guid = model.create(
                    subscription_guid=subscription_guid2,
                    transaction_type=model.TYPE_CHARGE,
                    amount=10,
                    payment_uri='/v1/cards/tester',
                    scheduled_at=datetime.datetime.utcnow(),
                )
                guid_ids2.append(guid)

        result_guids = [tx.guid for tx in 
                        model.list_by_subscription_guid(subscription_guid1)]
        self.assertEqual(set(result_guids), set(guid_ids1))

        result_guids = [tx.guid for tx in 
                        model.list_by_subscription_guid(subscription_guid2)]
        self.assertEqual(set(result_guids), set(guid_ids2))

    def test_list_by_company_guid_with_offset_limit(self):
        model = self.make_one(self.session)
        guids = []
        with db_transaction.manager:
            for i in range(10):
                with freeze_time('2013-08-16 00:00:{:02}'.format(i)):
                    guid = model.create(
                        subscription_guid=self.subscription_guid,
                        transaction_type=model.TYPE_CHARGE,
                        amount=10 * i,
                        payment_uri='/v1/cards/tester',
                        scheduled_at=datetime.datetime.utcnow(),
                    )
                    guids.append(guid)
        guids = list(reversed(guids))

        def assert_list(offset, limit, expected):
            result = model.list_by_company_guid(
                self.company_guid, 
                offset=offset, 
                limit=limit,
            )
            result_guids = [tx.guid for tx in result]
            self.assertEqual(set(result_guids), set(expected))
        assert_list(0, 0, [])
        assert_list(10, 10, [])
        assert_list(0, 10, guids)
        assert_list(0, 1, guids[:1])
        assert_list(1, 1, guids[1:2])
        assert_list(5, 1000, guids[5:])
        assert_list(5, 10, guids[5:])

    def test_create(self):
        model = self.make_one(self.session)

        subscription_guid = self.subscription_guid
        transaction_type = model.TYPE_CHARGE
        amount = 100
        payment_uri = '/v1/cards/tester'
        now = datetime.datetime.utcnow()
        scheduled_at = now + datetime.timedelta(days=1)

        with db_transaction.manager:
            guid = model.create(
                subscription_guid=subscription_guid,
                transaction_type=transaction_type,
                amount=amount,
                payment_uri=payment_uri,
                scheduled_at=scheduled_at,
            )

        transaction = model.get(guid)
        self.assertEqual(transaction.guid, guid)
        self.assert_(transaction.guid.startswith('TX'))
        self.assertEqual(transaction.subscription_guid, subscription_guid)
        self.assertEqual(transaction.transaction_type, transaction_type)
        self.assertEqual(transaction.amount, amount)
        self.assertEqual(transaction.payment_uri, payment_uri)
        self.assertEqual(transaction.status, model.STATUS_INIT)
        self.assertEqual(transaction.failure_count, 0)
        self.assertEqual(transaction.error_message, None)
        self.assertEqual(transaction.scheduled_at, scheduled_at)
        self.assertEqual(transaction.created_at, now)
        self.assertEqual(transaction.updated_at, now)

    def test_create_different_created_updated_time(self):
        from billy.models import tables
        model = self.make_one(self.session)

        results = [
            datetime.datetime(2013, 8, 16, 1),
            datetime.datetime(2013, 8, 16, 2),
        ]

        def mock_utcnow():
            return results.pop(0)

        tables.set_now_func(mock_utcnow)

        with db_transaction.manager:
            guid = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_CHARGE,
                amount=100,
                payment_uri='/v1/cards/tester',
                scheduled_at=datetime.datetime.utcnow(),
            )

        transaction = model.get(guid)
        self.assertEqual(transaction.created_at, transaction.updated_at)

    def test_create_refund(self):
        model = self.make_one(self.session)

        now = datetime.datetime.utcnow()

        with db_transaction.manager:
            tx_guid = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_CHARGE,
                amount=100,
                payment_uri='/v1/cards/tester',
                scheduled_at=now,
            )

        with db_transaction.manager:
            refund_guid = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_REFUND,
                refund_to_guid=tx_guid, 
                amount=50,
                scheduled_at=now,
            )

        refund_transaction = model.get(refund_guid)
        self.assertEqual(refund_transaction.refund_to_guid, tx_guid)
        self.assertEqual(refund_transaction.refund_to.guid, tx_guid)
        self.assertEqual(refund_transaction.refund_to.refund_from.guid, 
                         refund_guid)
        self.assertEqual(refund_transaction.transaction_type, model.TYPE_REFUND)
        self.assertEqual(refund_transaction.amount, decimal.Decimal(50))

    def test_create_refund_with_non_exist_target(self):
        model = self.make_one(self.session)
        now = datetime.datetime.utcnow()

        with self.assertRaises(KeyError):
            model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_REFUND,
                refund_to_guid='TX_NON_EXIST', 
                amount=50,
                scheduled_at=now,
            )

    def test_create_refund_with_wrong_transaction_type(self):
        model = self.make_one(self.session)
        now = datetime.datetime.utcnow()

        with self.assertRaises(ValueError):
            tx_guid = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_CHARGE,
                amount=100,
                payment_uri='/v1/cards/tester',
                scheduled_at=now,
            )
            model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_PAYOUT,
                refund_to_guid=tx_guid, 
                amount=50,
                scheduled_at=now,
            )

    def test_create_refund_with_payment_uri(self):
        model = self.make_one(self.session)
        now = datetime.datetime.utcnow()

        with self.assertRaises(ValueError):
            tx_guid = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_CHARGE,
                amount=100,
                payment_uri='/v1/cards/tester',
                scheduled_at=now,
            )
            model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_REFUND,
                refund_to_guid=tx_guid, 
                amount=50,
                scheduled_at=now,
                payment_uri='/v1/cards/tester',
            )

    def test_create_refund_with_wrong_target(self):
        model = self.make_one(self.session)
        now = datetime.datetime.utcnow()

        with db_transaction.manager:
            tx_guid = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_CHARGE,
                amount=100,
                payment_uri='/v1/cards/tester',
                scheduled_at=now,
            )
            refund_guid = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_REFUND,
                refund_to_guid=tx_guid, 
                amount=50,
                scheduled_at=now,
            )

        with self.assertRaises(ValueError):
            model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_REFUND,
                refund_to_guid=refund_guid, 
                amount=50,
                scheduled_at=now,
            )

        with db_transaction.manager:
            tx_guid = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_PAYOUT,
                amount=100,
                payment_uri='/v1/cards/tester',
                scheduled_at=now,
            )

        with self.assertRaises(ValueError):
            model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_REFUND,
                refund_to_guid=refund_guid, 
                amount=50,
                scheduled_at=now,
            )

    def test_create_with_wrong_type(self):
        model = self.make_one(self.session)

        with self.assertRaises(ValueError):
            model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=999,
                amount=123,
                payment_uri='/v1/cards/tester',
                scheduled_at=datetime.datetime.utcnow(),
            )

    def test_update(self):
        model = self.make_one(self.session)

        with db_transaction.manager:
            guid = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_CHARGE,
                amount=10,
                payment_uri='/v1/cards/tester',
                scheduled_at=datetime.datetime.utcnow(),
            )

        transaction = model.get(guid)
        status = model.STATUS_DONE

        with db_transaction.manager:
            model.update(
                guid=guid,
                status=status,
            )

        transaction = model.get(guid)
        self.assertEqual(transaction.status, status)

    def test_update_updated_at(self):
        model = self.make_one(self.session)

        with db_transaction.manager:
            guid = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_CHARGE,
                amount=10,
                payment_uri='/v1/cards/tester',
                scheduled_at=datetime.datetime.utcnow(),
            )

        transaction = model.get(guid)
        created_at = transaction.created_at

        # advanced the current date time
        with freeze_time('2013-08-16 07:00:01'):
            with db_transaction.manager:
                model.update(guid=guid)
            updated_at = datetime.datetime.utcnow()

        transaction = model.get(guid)
        self.assertEqual(transaction.updated_at, updated_at)
        self.assertEqual(transaction.created_at, created_at)

        # advanced the current date time even more
        with freeze_time('2013-08-16 08:35:40'):
            # this should update the updated_at field only
            with db_transaction.manager:
                model.update(guid)
            updated_at = datetime.datetime.utcnow()

        transaction = model.get(guid)
        self.assertEqual(transaction.updated_at, updated_at)
        self.assertEqual(transaction.created_at, created_at)

    def test_update_with_wrong_args(self):
        model = self.make_one(self.session)

        with db_transaction.manager:
            guid = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_CHARGE,
                amount=10,
                payment_uri='/v1/cards/tester',
                scheduled_at=datetime.datetime.utcnow(),
            )

        # make sure passing wrong argument will raise error
        with self.assertRaises(TypeError):
            model.update(
                guid=guid, 
                wrong_arg=True, 
                status=model.STATUS_INIT
            )

    def test_update_with_wrong_status(self):
        model = self.make_one(self.session)

        with db_transaction.manager:
            guid = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_CHARGE,
                amount=10,
                payment_uri='/v1/cards/tester',
                scheduled_at=datetime.datetime.utcnow(),
            )

        with self.assertRaises(ValueError):
            model.update(
                guid=guid,
                status=999,
            )

    def test_base_processor(self):
        from billy.models.processors.base import PaymentProcessor
        processor = PaymentProcessor()
        with self.assertRaises(NotImplementedError):
            processor.create_customer(None)
        with self.assertRaises(NotImplementedError):
            processor.prepare_customer(None)
        with self.assertRaises(NotImplementedError):
            processor.charge(None)
        with self.assertRaises(NotImplementedError):
            processor.payout(None)

    def test_process_one_charge(self):
        model = self.make_one(self.session)
        now = datetime.datetime.utcnow()

        payment_uri = '/v1/cards/tester'

        with db_transaction.manager:
            guid = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_CHARGE,
                amount=100,
                payment_uri=payment_uri,
                scheduled_at=now,
            )

        transaction = model.get(guid)
        customer = transaction.subscription.customer

        mock_processor = flexmock()
        (
            mock_processor 
            .should_receive('create_customer')
            .with_args(customer)
            .replace_with(lambda c: 'AC_MOCK')
            .once()
        )
        (
            mock_processor 
            .should_receive('prepare_customer')
            .with_args(customer, payment_uri)
            .replace_with(lambda c, payment_uri: None)
            .once()
        )
        (
            mock_processor 
            .should_receive('charge')
            .with_args(transaction)
            .replace_with(lambda t: 'TX_MOCK')
            .once()
        )

        with freeze_time('2013-08-20'):
            with db_transaction.manager:
                model.process_one(mock_processor, transaction)
                updated_at = datetime.datetime.utcnow()

        transaction = model.get(guid)
        self.assertEqual(transaction.status, model.STATUS_DONE)
        self.assertEqual(transaction.external_id, 'TX_MOCK')
        self.assertEqual(transaction.updated_at, updated_at)
        self.assertEqual(transaction.scheduled_at, now)
        self.assertEqual(transaction.created_at, now)
        self.assertEqual(transaction.subscription.customer.external_id, 
                         'AC_MOCK')

    def test_process_one_payout(self):
        model = self.make_one(self.session)
        now = datetime.datetime.utcnow()

        payment_uri = '/v1/cards/tester'

        with db_transaction.manager:
            guid = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_PAYOUT,
                amount=100,
                payment_uri=payment_uri,
                scheduled_at=now,
            )

        transaction = model.get(guid)
        customer = transaction.subscription.customer

        mock_processor = flexmock()
        (
            mock_processor 
            .should_receive('create_customer')
            .with_args(customer)
            .replace_with(lambda c: 'AC_MOCK')
            .once()
        )
        (
            mock_processor 
            .should_receive('prepare_customer')
            .with_args(customer, payment_uri)
            .replace_with(lambda c, payment_uri: None)
            .once()
        )
        (
            mock_processor 
            .should_receive('payout')
            .with_args(transaction)
            .replace_with(lambda t: 'TX_MOCK')
            .once()
        )

        with freeze_time('2013-08-20'):
            with db_transaction.manager:
                model.process_one(mock_processor, transaction)
                updated_at = datetime.datetime.utcnow()

        transaction = model.get(guid)
        self.assertEqual(transaction.status, model.STATUS_DONE)
        self.assertEqual(transaction.external_id, 'TX_MOCK')
        self.assertEqual(transaction.updated_at, updated_at)
        self.assertEqual(transaction.scheduled_at, now)
        self.assertEqual(transaction.created_at, now)
        self.assertEqual(transaction.subscription.customer.external_id, 
                         'AC_MOCK')

    def test_process_one_with_failure(self):
        model = self.make_one(self.session)
        now = datetime.datetime.utcnow()

        payment_uri = '/v1/cards/tester'

        with db_transaction.manager:
            guid = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_CHARGE,
                amount=100,
                payment_uri=payment_uri,
                scheduled_at=now,
            )

        transaction = model.get(guid)
        customer = transaction.subscription.customer

        def mock_charge(transaction):
            raise RuntimeError('Failed to charge')

        mock_processor = flexmock()
        (
            mock_processor 
            .should_receive('create_customer')
            .with_args(customer)
            .replace_with(lambda c: 'AC_MOCK')
            .once()
        )
        (
            mock_processor 
            .should_receive('prepare_customer')
            .with_args(customer, payment_uri)
            .replace_with(lambda c, payment_uri: None)
            .once()
        )
        (
            mock_processor 
            .should_receive('charge')
            .with_args(transaction)
            .replace_with(mock_charge)
            .once()
        )

        with db_transaction.manager:
            model.process_one(mock_processor, transaction)
            updated_at = datetime.datetime.utcnow()

        transaction = model.get(guid)
        self.assertEqual(transaction.status, model.STATUS_RETRYING)
        self.assertEqual(transaction.updated_at, updated_at)
        self.assertEqual(transaction.failure_count, 1)
        self.assertEqual(transaction.error_message, 'Failed to charge')
        self.assertEqual(transaction.subscription.customer.external_id, 
                         'AC_MOCK')

    def test_process_one_with_failure_exceed_limitation(self):
        model = self.make_one(self.session)
        now = datetime.datetime.utcnow()

        payment_uri = '/v1/cards/tester'
        maximum_retry = 3

        with db_transaction.manager:
            self.customer_model.update(self.customer_guid, external_id='AC_MOCK')
            guid = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_CHARGE,
                amount=100,
                payment_uri=payment_uri,
                scheduled_at=now,
            )

        transaction = model.get(guid)

        def mock_charge(transaction):
            raise RuntimeError('Failed to charge')

        mock_processor = flexmock()
        (
            mock_processor 
            .should_receive('prepare_customer')
            .replace_with(lambda c, payment_uri: None)
            .times(4)
        )
        (
            mock_processor 
            .should_receive('charge')
            .replace_with(mock_charge)
            .times(4)
        )

        for _ in range(3):
            with db_transaction.manager:
                transaction = model.get(guid)
                model.process_one(
                    processor=mock_processor, 
                    transaction=transaction, 
                    maximum_retry=maximum_retry
                )
            transaction = model.get(guid)
            self.assertEqual(transaction.status, model.STATUS_RETRYING)
        with db_transaction.manager:
            transaction = model.get(guid)
            model.process_one(
                processor=mock_processor, 
                transaction=transaction, 
                maximum_retry=maximum_retry
            )
        transaction = model.get(guid)
        self.assertEqual(transaction.status, model.STATUS_FAILED)

    def test_process_one_with_system_exit_and_keyboard_interrupt(self):
        model = self.make_one(self.session)
        now = datetime.datetime.utcnow()

        payment_uri = '/v1/cards/tester'

        with db_transaction.manager:
            guid = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_CHARGE,
                amount=100,
                payment_uri=payment_uri,
                scheduled_at=now,
            )

        transaction = model.get(guid)

        def mock_create_customer_system_exit(transaction):
            raise SystemExit

        mock_processor = flexmock()
        (
            mock_processor 
            .should_receive('create_customer')
            .replace_with(mock_create_customer_system_exit)
        )

        with self.assertRaises(SystemExit):
            model.process_one(mock_processor, transaction)

        def mock_create_customer_keyboard_interrupt(transaction):
            raise KeyboardInterrupt

        mock_processor = flexmock()
        (
            mock_processor 
            .should_receive('create_customer')
            .replace_with(mock_create_customer_keyboard_interrupt)
        )

        with self.assertRaises(KeyboardInterrupt):
            model.process_one(mock_processor, transaction)

    def test_process_one_with_already_done(self):
        model = self.make_one(self.session)
        now = datetime.datetime.utcnow()

        payment_uri = '/v1/cards/tester'

        with db_transaction.manager:
            guid = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_CHARGE,
                amount=100,
                payment_uri=payment_uri,
                scheduled_at=now,
            )
            transaction = model.get(guid)
            transaction.status = model.STATUS_DONE
            self.session.add(transaction)

        processor = flexmock()
        transaction = model.get(guid)
        with self.assertRaises(ValueError):
            model.process_one(processor, transaction)

    def test_process_transactions(self):
        model = self.make_one(self.session)
        now = datetime.datetime.utcnow()

        payment_uri = '/v1/cards/tester'

        with db_transaction.manager:
            guid1 = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_CHARGE,
                amount=100,
                payment_uri=payment_uri,
                scheduled_at=now,
            )

            guid2 = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_CHARGE,
                amount=100,
                payment_uri=payment_uri,
                scheduled_at=now,
            )
            model.update(guid2, status=model.STATUS_RETRYING)

            guid3 = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_CHARGE,
                amount=100,
                payment_uri=payment_uri,
                scheduled_at=now,
            )
            
            guid4 = model.create(
                subscription_guid=self.subscription_guid,
                transaction_type=model.TYPE_CHARGE,
                amount=100,
                payment_uri=payment_uri,
                scheduled_at=now,
            )
            model.update(guid4, status=model.STATUS_DONE)

        processor = flexmock()
        with db_transaction.manager:
            tx_guids = model.process_transactions(processor)

        self.assertEqual(set(tx_guids), set([guid1, guid2, guid3]))