Пример #1
0
def main(argv=sys.argv, processor=None):
    logger = logging.getLogger(__name__)

    if len(argv) != 2:
        usage(argv)
    config_uri = argv[1]
    setup_logging(config_uri)
    settings = get_appsettings(config_uri)
    settings = setup_database({}, **settings)

    session = settings['session']
    subscription_model = SubscriptionModel(session)
    tx_model = TransactionModel(session)

    maximum_retry = int(settings.get(
        'billy.transaction.maximum_retry', 
        TransactionModel.DEFAULT_MAXIMUM_RETRY,
    ))

    resolver = DottedNameResolver()
    if processor is None:
        processor_factory = settings['billy.processor_factory']
        processor_factory = resolver.maybe_resolve(processor_factory)
        processor = processor_factory()

    # yield all transactions and commit before we process them, so that
    # we won't double process them. 
    with db_transaction.manager:
        logger.info('Yielding transaction ...')
        subscription_model.yield_transactions()

    with db_transaction.manager:
        logger.info('Processing transaction ...')
        tx_model.process_transactions(processor, maximum_retry=maximum_retry)
    logger.info('Done')
Пример #2
0
    def test_cancel_subscription(self):
        from billy.models.subscription import SubscriptionModel
        from billy.models.transaction import TransactionModel

        subscription_model = SubscriptionModel(self.testapp.session)
        tx_model = TransactionModel(self.testapp.session)
        now = datetime.datetime.utcnow()

        with db_transaction.manager:
            subscription_guid = subscription_model.create(
                customer_guid=self.customer_guid,
                plan_guid=self.plan_guid,
            )
            tx_model.create(
                subscription_guid=subscription_guid,
                transaction_type=tx_model.TYPE_CHARGE,
                amount=100, 
                scheduled_at=now,
            )

        with freeze_time('2013-08-16 07:00:00'):
            canceled_at = datetime.datetime.utcnow()
            res = self.testapp.post(
                '/v1/subscriptions/{}/cancel'.format(subscription_guid), 
                extra_environ=dict(REMOTE_USER=self.api_key), 
                status=200,
            )

        subscription = res.json
        self.assertEqual(subscription['canceled'], True)
        self.assertEqual(subscription['canceled_at'], canceled_at.isoformat())
Пример #3
0
    def test_cancel_a_canceled_subscription(self):
        from billy.models.subscription import SubscriptionModel
        from billy.models.transaction import TransactionModel

        subscription_model = SubscriptionModel(self.testapp.session)
        tx_model = TransactionModel(self.testapp.session)
        now = datetime.datetime.utcnow()

        with db_transaction.manager:
            subscription_guid = subscription_model.create(
                customer_guid=self.customer_guid,
                plan_guid=self.plan_guid,
            )
            tx_model.create(
                subscription_guid=subscription_guid,
                transaction_type=tx_model.TYPE_CHARGE,
                amount=100, 
                scheduled_at=now,
            )

        self.testapp.post(
            '/v1/subscriptions/{}/cancel'.format(subscription_guid), 
            extra_environ=dict(REMOTE_USER=self.api_key), 
            status=200,
        )
        self.testapp.post(
            '/v1/subscriptions/{}/cancel'.format(subscription_guid), 
            extra_environ=dict(REMOTE_USER=self.api_key), 
            status=400,
        )
Пример #4
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
     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',
         )
Пример #5
0
    def test_cancel_subscription(self):
        from billy.models.subscription import SubscriptionModel
        from billy.models.transaction import TransactionModel

        subscription_model = SubscriptionModel(self.testapp.session)
        tx_model = TransactionModel(self.testapp.session)
        now = datetime.datetime.utcnow()

        with db_transaction.manager:
            subscription_guid = subscription_model.create(
                customer_guid=self.customer_guid,
                plan_guid=self.plan_guid,
            )
            tx_model.create(
                subscription_guid=subscription_guid,
                transaction_type=tx_model.TYPE_CHARGE,
                amount=100,
                scheduled_at=now,
            )

        with freeze_time('2013-08-16 07:00:00'):
            canceled_at = datetime.datetime.utcnow()
            res = self.testapp.post(
                '/v1/subscriptions/{}/cancel'.format(subscription_guid),
                extra_environ=dict(REMOTE_USER=self.api_key),
                status=200,
            )

        subscription = res.json
        self.assertEqual(subscription['canceled'], True)
        self.assertEqual(subscription['canceled_at'], canceled_at.isoformat())
Пример #6
0
    def test_transaction_list_by_subscription(self):
        from billy.models.transaction import TransactionModel
        from billy.models.subscription import SubscriptionModel
        subscription_model = SubscriptionModel(self.testapp.session)
        transaction_model = TransactionModel(self.testapp.session)
        with db_transaction.manager:
            subscription_guid1 = subscription_model.create(
                customer_guid=self.customer_guid,
                plan_guid=self.plan_guid,
            )
            subscription_guid2 = subscription_model.create(
                customer_guid=self.customer_guid,
                plan_guid=self.plan_guid,
            )
        guids1 = []
        guids2 = []
        with db_transaction.manager:
            for i in range(10):
                with freeze_time('2013-08-16 00:00:{:02}'.format(i + 1)):
                    guid = transaction_model.create(
                        subscription_guid=subscription_guid1,
                        transaction_type=transaction_model.TYPE_CHARGE,
                        amount=10 * i,
                        payment_uri='/v1/cards/tester',
                        scheduled_at=datetime.datetime.utcnow(),
                    )
                    guids1.append(guid)
            for i in range(20):
                with freeze_time('2013-08-16 00:00:{:02}'.format(i + 1)):
                    guid = transaction_model.create(
                        subscription_guid=subscription_guid2,
                        transaction_type=transaction_model.TYPE_CHARGE,
                        amount=10 * i,
                        payment_uri='/v1/cards/tester',
                        scheduled_at=datetime.datetime.utcnow(),
                    )
                    guids2.append(guid)
        guids1 = list(reversed(guids1))
        guids2 = list(reversed(guids2))

        res = self.testapp.get(
            '/v1/subscriptions/{}/transactions'.format(subscription_guid1),
            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, guids1)

        res = self.testapp.get(
            '/v1/subscriptions/{}/transactions'.format(subscription_guid2),
            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, guids2)
Пример #7
0
    def test_transaction_list_by_subscription(self):
        from billy.models.transaction import TransactionModel
        from billy.models.subscription import SubscriptionModel
        subscription_model = SubscriptionModel(self.testapp.session)
        transaction_model = TransactionModel(self.testapp.session)
        with db_transaction.manager:
            subscription_guid1 = subscription_model.create(
                customer_guid=self.customer_guid,
                plan_guid=self.plan_guid,
            )
            subscription_guid2 = subscription_model.create(
                customer_guid=self.customer_guid,
                plan_guid=self.plan_guid,
            )
        guids1 = []
        guids2 = []
        with db_transaction.manager:
            for i in range(10):
                with freeze_time('2013-08-16 00:00:{:02}'.format(i + 1)):
                    guid = transaction_model.create(
                        subscription_guid=subscription_guid1,
                        transaction_type=transaction_model.TYPE_CHARGE,
                        amount=10 * i,
                        payment_uri='/v1/cards/tester',
                        scheduled_at=datetime.datetime.utcnow(),
                    )
                    guids1.append(guid)
            for i in range(20):
                with freeze_time('2013-08-16 00:00:{:02}'.format(i + 1)):
                    guid = transaction_model.create(
                        subscription_guid=subscription_guid2,
                        transaction_type=transaction_model.TYPE_CHARGE,
                        amount=10 * i,
                        payment_uri='/v1/cards/tester',
                        scheduled_at=datetime.datetime.utcnow(),
                    )
                    guids2.append(guid)
        guids1 = list(reversed(guids1))
        guids2 = list(reversed(guids2))

        res = self.testapp.get(
            '/v1/subscriptions/{}/transactions'.format(subscription_guid1),
            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, guids1)

        res = self.testapp.get(
            '/v1/subscriptions/{}/transactions'.format(subscription_guid2),
            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, guids2)
Пример #8
0
def subscription_cancel(request):
    """Cancel a subscription

    """
    # TODO: it appears a DELETE request with body is not a good idea
    # for HTTP protocol as many server doesn't support this, this is why
    # we use another view with post method, maybe we should use a better 
    # approach later
    company = auth_api_key(request)
    form = validate_form(SubscriptionCancelForm, request)

    guid = request.matchdict['subscription_guid']
    prorated_refund = asbool(form.data.get('prorated_refund', False))
    refund_amount = form.data.get('refund_amount')
    maximum_retry = int(request.registry.settings.get(
        'billy.transaction.maximum_retry', 
        TransactionModel.DEFAULT_MAXIMUM_RETRY,
    ))

    model = SubscriptionModel(request.session)
    tx_model = TransactionModel(request.session)
    get_and_check_subscription(request, company, guid)
    subscription = model.get(guid)

    # TODO: maybe we can find a better way to integrate this with the 
    # form validation?
    if refund_amount is not None:
        if subscription.amount is not None:
            amount = subscription.amount
        else:
            amount = subscription.plan.amount
        if refund_amount > amount:
            return form_errors_to_bad_request(dict(
                refund_amount=['refund_amount cannot be greater than '
                               'subscription amount {}'.format(amount)]
            ))

    if subscription.canceled:
        return HTTPBadRequest('Cannot cancel a canceled subscription')

    with db_transaction.manager:
        tx_guid = model.cancel(
            guid, 
            prorated_refund=prorated_refund,
            refund_amount=refund_amount, 
        )
    if tx_guid is not None:
        with db_transaction.manager:
            tx_model.process_transactions(
                processor=request.processor, 
                guids=[tx_guid],
                maximum_retry=maximum_retry,
            )

    subscription = model.get(guid)
    return subscription 
Пример #9
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
Пример #10
0
    def test_create_subscription(self):
        from billy.models.subscription import SubscriptionModel
        from billy.models.transaction import TransactionModel

        customer_guid = self.customer_guid
        plan_guid = self.plan_guid
        amount = '55.66'
        payment_uri = 'MOCK_CARD_URI'
        now = datetime.datetime.utcnow()
        now_iso = now.isoformat()
        # next week
        next_transaction_at = datetime.datetime(2013, 8, 23)
        next_iso = next_transaction_at.isoformat()

        def mock_charge(transaction):
            self.assertEqual(transaction.subscription.customer_guid,
                             customer_guid)
            self.assertEqual(transaction.subscription.plan_guid, plan_guid)
            return 'MOCK_PROCESSOR_TRANSACTION_ID'

        mock_processor = flexmock(DummyProcessor)
        (mock_processor.should_receive('create_customer').once())

        (mock_processor.should_receive('charge').replace_with(
            mock_charge).once())

        res = self.testapp.post(
            '/v1/subscriptions',
            dict(
                customer_guid=customer_guid,
                plan_guid=plan_guid,
                amount=amount,
                payment_uri=payment_uri,
            ),
            extra_environ=dict(REMOTE_USER=self.api_key),
            status=200,
        )
        self.failUnless('guid' in res.json)
        self.assertEqual(res.json['created_at'], now_iso)
        self.assertEqual(res.json['updated_at'], now_iso)
        self.assertEqual(res.json['canceled_at'], None)
        self.assertEqual(res.json['next_transaction_at'], next_iso)
        self.assertEqual(res.json['period'], 1)
        self.assertEqual(res.json['amount'], amount)
        self.assertEqual(res.json['customer_guid'], customer_guid)
        self.assertEqual(res.json['plan_guid'], plan_guid)
        self.assertEqual(res.json['payment_uri'], payment_uri)

        subscription_model = SubscriptionModel(self.testapp.session)
        subscription = subscription_model.get(res.json['guid'])
        self.assertEqual(len(subscription.transactions), 1)
        transaction = subscription.transactions[0]
        self.assertEqual(transaction.external_id,
                         'MOCK_PROCESSOR_TRANSACTION_ID')
        self.assertEqual(transaction.status, TransactionModel.STATUS_DONE)
Пример #11
0
def subscription_cancel(request):
    """Cancel a subscription

    """
    # TODO: it appears a DELETE request with body is not a good idea
    # for HTTP protocol as many server doesn't support this, this is why
    # we use another view with post method, maybe we should use a better 
    # approach later
    company = auth_api_key(request)
    form = validate_form(SubscriptionCancelForm, request)

    guid = request.matchdict['subscription_guid']
    prorated_refund = asbool(form.data.get('prorated_refund', False))
    refund_amount = form.data.get('refund_amount')
    maximum_retry = int(request.registry.settings.get(
        'billy.transaction.maximum_retry', 
        TransactionModel.DEFAULT_MAXIMUM_RETRY,
    ))

    model = SubscriptionModel(request.session)
    tx_model = TransactionModel(request.session)
    get_and_check_subscription(request, company, guid)

    # TODO: maybe we can find a better way to integrate this with the 
    # form validation?
    if refund_amount is not None:
        subscription = model.get(guid)
        if subscription.amount is not None:
            amount = subscription.amount
        else:
            amount = subscription.plan.amount
        if refund_amount > amount:
            return form_errors_to_bad_request(dict(
                refund_amount=['refund_amount cannot be greater than '
                               'subscription amount {}'.format(amount)]
            ))

    # TODO: make sure the subscription is not already canceled

    with db_transaction.manager:
        tx_guid = model.cancel(
            guid, 
            prorated_refund=prorated_refund,
            refund_amount=refund_amount, 
        )
    if tx_guid is not None:
        with db_transaction.manager:
            tx_model.process_transactions(
                processor=request.processor, 
                guids=[tx_guid],
                maximum_retry=maximum_retry,
            )

    subscription = model.get(guid)
    return subscription 
Пример #12
0
    def test_transaction_list_by_subscription_with_bad_api_key(self):
        from billy.models.subscription import SubscriptionModel

        subscription_model = SubscriptionModel(self.testapp.session)
        with db_transaction.manager:
            subscription_guid = subscription_model.create(customer_guid=self.customer_guid, plan_guid=self.plan_guid)

        self.testapp.get(
            "/v1/subscriptions/{}/transactions".format(subscription_guid),
            extra_environ=dict(REMOTE_USER=b"BAD_API_KEY"),
            status=403,
        )
Пример #13
0
def get_and_check_subscription(request, company, guid):
    """Get and check permission to access a subscription

    """
    model = SubscriptionModel(request.session)
    subscription = model.get(guid)
    if subscription is None:
        raise HTTPNotFound('No such subscription {}'.format(guid))
    if subscription.customer.company_guid != company.guid:
        raise HTTPForbidden('You have no permission to access subscription {}'
                            .format(guid))
    return subscription
Пример #14
0
    def test_cancel_subscription_with_refund_amount(self):
        from billy.models.subscription import SubscriptionModel
        from billy.models.transaction import TransactionModel

        subscription_model = SubscriptionModel(self.testapp.session)
        tx_model = TransactionModel(self.testapp.session)
        now = datetime.datetime.utcnow()

        with db_transaction.manager:
            subscription_guid = subscription_model.create(customer_guid=self.customer_guid, plan_guid=self.plan_guid)
            tx_guid = tx_model.create(
                subscription_guid=subscription_guid,
                transaction_type=tx_model.TYPE_CHARGE,
                amount=1000,
                scheduled_at=now,
            )
            subscription = subscription_model.get(subscription_guid)
            subscription.period = 1
            subscription.next_transaction_at = datetime.datetime(2013, 8, 23)
            self.testapp.session.add(subscription)

            transaction = tx_model.get(tx_guid)
            transaction.status = tx_model.STATUS_DONE
            transaction.external_id = "MOCK_BALANCED_DEBIT_URI"
            self.testapp.session.add(transaction)

        refund_called = []

        def mock_refund(transaction):
            refund_called.append(transaction)
            return "MOCK_PROCESSOR_REFUND_URI"

        mock_processor = flexmock(DummyProcessor)
        (mock_processor.should_receive("refund").replace_with(mock_refund).once())

        res = self.testapp.post(
            "/v1/subscriptions/{}/cancel".format(subscription_guid),
            dict(refund_amount=234),
            extra_environ=dict(REMOTE_USER=self.api_key),
            status=200,
        )
        subscription = res.json

        transaction = refund_called[0]
        self.testapp.session.add(transaction)
        self.assertEqual(transaction.refund_to.guid, tx_guid)
        self.assertEqual(transaction.subscription_guid, subscription_guid)
        self.assertEqual(transaction.amount, 234)
        self.assertEqual(transaction.status, tx_model.STATUS_DONE)

        res = self.testapp.get("/v1/transactions", extra_environ=dict(REMOTE_USER=self.api_key), status=200)
        guids = [item["guid"] for item in res.json["items"]]
        self.assertEqual(set(guids), set([tx_guid, transaction.guid]))
Пример #15
0
def get_and_check_subscription(request, company, guid):
    """Get and check permission to access a subscription

    """
    model = SubscriptionModel(request.session)
    subscription = model.get(guid)
    if subscription is None:
        raise HTTPNotFound('No such subscription {}'.format(guid))
    if subscription.customer.company_guid != company.guid:
        raise HTTPForbidden('You have no permission to access subscription {}'
                            .format(guid))
    return subscription
Пример #16
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')
    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')
    plan = plan_model.get(plan_guid)
    if plan.company_guid != company.guid:
        return HTTPForbidden('Can only subscribe to your own plan')
    # TODO: make sure user cannot subscribe to a deleted plan or customer

    # 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
Пример #17
0
    def test_transaction_list_by_subscription_with_bad_api_key(self):
        from billy.models.subscription import SubscriptionModel
        subscription_model = SubscriptionModel(self.testapp.session)
        with db_transaction.manager:
            subscription_guid = subscription_model.create(
                customer_guid=self.customer_guid,
                plan_guid=self.plan_guid,
            )

        self.testapp.get(
            '/v1/subscriptions/{}/transactions'.format(subscription_guid),
            extra_environ=dict(REMOTE_USER=b'BAD_API_KEY'),
            status=403,
        )
Пример #18
0
    def test_create_subscription(self):
        from billy.models.subscription import SubscriptionModel
        from billy.models.transaction import TransactionModel

        customer_guid = self.customer_guid
        plan_guid = self.plan_guid
        amount = 5566
        payment_uri = "MOCK_CARD_URI"
        now = datetime.datetime.utcnow()
        now_iso = now.isoformat()
        # next week
        next_transaction_at = datetime.datetime(2013, 8, 23)
        next_iso = next_transaction_at.isoformat()

        def mock_charge(transaction):
            self.assertEqual(transaction.subscription.customer_guid, customer_guid)
            self.assertEqual(transaction.subscription.plan_guid, plan_guid)
            return "MOCK_PROCESSOR_TRANSACTION_ID"

        mock_processor = flexmock(DummyProcessor)
        (mock_processor.should_receive("create_customer").once())

        (mock_processor.should_receive("charge").replace_with(mock_charge).once())

        res = self.testapp.post(
            "/v1/subscriptions",
            dict(customer_guid=customer_guid, plan_guid=plan_guid, amount=amount, payment_uri=payment_uri),
            extra_environ=dict(REMOTE_USER=self.api_key),
            status=200,
        )
        self.failUnless("guid" in res.json)
        self.assertEqual(res.json["created_at"], now_iso)
        self.assertEqual(res.json["updated_at"], now_iso)
        self.assertEqual(res.json["canceled_at"], None)
        self.assertEqual(res.json["next_transaction_at"], next_iso)
        self.assertEqual(res.json["period"], 1)
        self.assertEqual(res.json["amount"], amount)
        self.assertEqual(res.json["customer_guid"], customer_guid)
        self.assertEqual(res.json["plan_guid"], plan_guid)
        self.assertEqual(res.json["payment_uri"], payment_uri)
        self.assertEqual(res.json["canceled"], False)

        subscription_model = SubscriptionModel(self.testapp.session)
        subscription = subscription_model.get(res.json["guid"])
        self.assertEqual(len(subscription.transactions), 1)
        transaction = subscription.transactions[0]
        self.assertEqual(transaction.external_id, "MOCK_PROCESSOR_TRANSACTION_ID")
        self.assertEqual(transaction.status, TransactionModel.STATUS_DONE)
Пример #19
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',
         )
Пример #20
0
    def test_subscription_list(self):
        from billy.models.subscription import SubscriptionModel

        subscription_model = SubscriptionModel(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 = subscription_model.create(customer_guid=self.customer_guid, plan_guid=self.plan_guid)
                    guids.append(guid)
        guids = list(reversed(guids))

        res = self.testapp.get("/v1/subscriptions", 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)
Пример #21
0
    def test_cancel_subscription_to_other_company(self):
        from billy.models.subscription import SubscriptionModel
        from billy.models.company import CompanyModel

        subscription_model = SubscriptionModel(self.testapp.session)
        company_model = CompanyModel(self.testapp.session)

        with db_transaction.manager:
            subscription_guid = subscription_model.create(customer_guid=self.customer_guid, plan_guid=self.plan_guid)
            other_company_guid = company_model.create(processor_key="MOCK_PROCESSOR_KEY")
            other_company = company_model.get(other_company_guid)
            other_api_key = str(other_company.api_key)

        self.testapp.post(
            "/v1/subscriptions/{}/cancel".format(subscription_guid),
            extra_environ=dict(REMOTE_USER=other_api_key),
            status=403,
        )
Пример #22
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())
Пример #23
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,
     )
Пример #24
0
    def test_cancel_subscription_with_bad_arguments(self):
        from billy.models.subscription import SubscriptionModel
        from billy.models.transaction import TransactionModel

        subscription_model = SubscriptionModel(self.testapp.session)
        tx_model = TransactionModel(self.testapp.session)
        now = datetime.datetime.utcnow()

        with db_transaction.manager:
            subscription_guid = subscription_model.create(
                customer_guid=self.customer_guid,
                plan_guid=self.plan_guid,
                amount=100,
            )
            tx_guid = tx_model.create(
                subscription_guid=subscription_guid,
                transaction_type=tx_model.TYPE_CHARGE,
                amount=100,
                scheduled_at=now,
            )
            subscription = subscription_model.get(subscription_guid)
            subscription.period = 1
            subscription.next_transaction_at = datetime.datetime(2013, 8, 23)
            self.testapp.session.add(subscription)

            transaction = tx_model.get(tx_guid)
            transaction.status = tx_model.STATUS_DONE
            transaction.external_id = 'MOCK_BALANCED_DEBIT_URI'
            self.testapp.session.add(transaction)

        def assert_bad_parameters(kwargs):
            self.testapp.post(
                '/v1/subscriptions/{}/cancel'.format(subscription_guid),
                kwargs,
                extra_environ=dict(REMOTE_USER=self.api_key),
                status=400,
            )

        assert_bad_parameters(dict(prorated_refund=True, refund_amount=10))
        assert_bad_parameters(dict(refund_amount='100.01'))
Пример #25
0
    def test_cancel_subscription_to_other_company(self):
        from billy.models.subscription import SubscriptionModel
        from billy.models.company import CompanyModel

        subscription_model = SubscriptionModel(self.testapp.session)
        company_model = CompanyModel(self.testapp.session)

        with db_transaction.manager:
            subscription_guid = subscription_model.create(
                customer_guid=self.customer_guid,
                plan_guid=self.plan_guid,
            )
            other_company_guid = company_model.create(
                processor_key='MOCK_PROCESSOR_KEY', )
            other_company = company_model.get(other_company_guid)
            other_api_key = str(other_company.api_key)

        self.testapp.post(
            '/v1/subscriptions/{}/cancel'.format(subscription_guid),
            extra_environ=dict(REMOTE_USER=other_api_key),
            status=403,
        )
Пример #26
0
    def test_subscription_list(self):
        from billy.models.subscription import SubscriptionModel
        subscription_model = SubscriptionModel(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 = subscription_model.create(
                        customer_guid=self.customer_guid,
                        plan_guid=self.plan_guid,
                    )
                    guids.append(guid)
        guids = list(reversed(guids))

        res = self.testapp.get(
            '/v1/subscriptions',
            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)
Пример #27
0
    def test_cancel_subscription_with_bad_arguments(self):
        from billy.models.subscription import SubscriptionModel
        from billy.models.transaction import TransactionModel

        subscription_model = SubscriptionModel(self.testapp.session)
        tx_model = TransactionModel(self.testapp.session)
        now = datetime.datetime.utcnow()

        with db_transaction.manager:
            subscription_guid = subscription_model.create(
                customer_guid=self.customer_guid,
                plan_guid=self.plan_guid,
                amount=100,
            )
            tx_guid = tx_model.create(
                subscription_guid=subscription_guid,
                transaction_type=tx_model.TYPE_CHARGE,
                amount=100, 
                scheduled_at=now,
            )
            subscription = subscription_model.get(subscription_guid)
            subscription.period = 1
            subscription.next_transaction_at = datetime.datetime(2013, 8, 23)
            self.testapp.session.add(subscription)

            transaction = tx_model.get(tx_guid)
            transaction.status = tx_model.STATUS_DONE
            transaction.external_id = 'MOCK_BALANCED_DEBIT_URI'
            self.testapp.session.add(transaction)

        def assert_bad_parameters(kwargs):
            self.testapp.post(
                '/v1/subscriptions/{}/cancel'.format(subscription_guid), 
                kwargs,
                extra_environ=dict(REMOTE_USER=self.api_key), 
                status=400,
            )
        assert_bad_parameters(dict(prorated_refund=True, refund_amount=10))
        assert_bad_parameters(dict(refund_amount='100.01'))
Пример #28
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()
Пример #29
0
    def test_subscription(self):
        from billy.models.subscription import SubscriptionModel
        from billy.renderers import subscription_adapter
        subscription_model = SubscriptionModel(self.testapp.session)
        subscription = subscription_model.get(self.subscription_guid)
        json_data = subscription_adapter(subscription, self.dummy_request)
        expected = dict(
            guid=subscription.guid, 
            amount=None,
            payment_uri=subscription.payment_uri,
            period=subscription.period,
            canceled=subscription.canceled,
            next_transaction_at=subscription.next_transaction_at.isoformat(),
            created_at=subscription.created_at.isoformat(),
            updated_at=subscription.updated_at.isoformat(),
            started_at=subscription.started_at.isoformat(),
            canceled_at=None,
            customer_guid=subscription.customer_guid,
            plan_guid=subscription.plan_guid,
        )
        self.assertEqual(json_data, expected)

        def assert_amount(amount, expected_amount):
            subscription.amount = amount 
            json_data = subscription_adapter(subscription, self.dummy_request)
            self.assertEqual(json_data['amount'], expected_amount)

        assert_amount(None, None)
        assert_amount(1234, 1234)

        def assert_canceled_at(canceled_at, expected_canceled_at):
            subscription.canceled_at = canceled_at 
            json_data = subscription_adapter(subscription, self.dummy_request)
            self.assertEqual(json_data['canceled_at'], expected_canceled_at)

        now = datetime.datetime.utcnow()
        assert_canceled_at(None, None)
        assert_canceled_at(now, now.isoformat())
Пример #30
0
def main(argv=sys.argv, processor=None):
    logger = logging.getLogger(__name__)

    if len(argv) != 2:
        usage(argv)
    config_uri = argv[1]
    setup_logging(config_uri)
    settings = get_appsettings(config_uri)
    settings = setup_database({}, **settings)

    session = settings['session']
    subscription_model = SubscriptionModel(session)
    tx_model = TransactionModel(session)

    maximum_retry = int(
        settings.get(
            'billy.transaction.maximum_retry',
            TransactionModel.DEFAULT_MAXIMUM_RETRY,
        ))

    resolver = DottedNameResolver()
    if processor is None:
        processor_factory = settings['billy.processor_factory']
        processor_factory = resolver.maybe_resolve(processor_factory)
        processor = processor_factory()

    # yield all transactions and commit before we process them, so that
    # we won't double process them.
    with db_transaction.manager:
        logger.info('Yielding transaction ...')
        subscription_model.yield_transactions()

    with db_transaction.manager:
        logger.info('Processing transaction ...')
        tx_model.process_transactions(processor, maximum_retry=maximum_retry)
    logger.info('Done')
Пример #31
0
 def make_one(self, *args, **kwargs):
     from billy.models.subscription import SubscriptionModel
     return SubscriptionModel(*args, **kwargs)
Пример #32
0
    def create_subscription_model(self):
        """Create a subscription model

        """
        return SubscriptionModel(self)
Пример #33
0
    def test_create_subscription(self):
        from billy.models.subscription import SubscriptionModel
        from billy.models.transaction import TransactionModel

        customer_guid = self.customer_guid
        plan_guid = self.plan_guid
        amount = '55.66'
        payment_uri = 'MOCK_CARD_URI'
        now = datetime.datetime.utcnow()
        now_iso = now.isoformat()
        # next week
        next_transaction_at = datetime.datetime(2013, 8, 23)
        next_iso = next_transaction_at.isoformat()

        def mock_charge(transaction):
            self.assertEqual(transaction.subscription.customer_guid, 
                             customer_guid)
            self.assertEqual(transaction.subscription.plan_guid, 
                             plan_guid)
            return 'MOCK_PROCESSOR_TRANSACTION_ID'

        mock_processor = flexmock(DummyProcessor)
        (
            mock_processor
            .should_receive('create_customer')
            .once()
        )

        (
            mock_processor
            .should_receive('charge')
            .replace_with(mock_charge)
            .once()
        )

        res = self.testapp.post(
            '/v1/subscriptions',
            dict(
                customer_guid=customer_guid,
                plan_guid=plan_guid,
                amount=amount,
                payment_uri=payment_uri,
            ),
            extra_environ=dict(REMOTE_USER=self.api_key), 
            status=200,
        )
        self.failUnless('guid' in res.json)
        self.assertEqual(res.json['created_at'], now_iso)
        self.assertEqual(res.json['updated_at'], now_iso)
        self.assertEqual(res.json['canceled_at'], None)
        self.assertEqual(res.json['next_transaction_at'], next_iso)
        self.assertEqual(res.json['period'], 1)
        self.assertEqual(res.json['amount'], amount)
        self.assertEqual(res.json['customer_guid'], customer_guid)
        self.assertEqual(res.json['plan_guid'], plan_guid)
        self.assertEqual(res.json['payment_uri'], payment_uri)

        subscription_model = SubscriptionModel(self.testapp.session)
        subscription = subscription_model.get(res.json['guid'])
        self.assertEqual(len(subscription.transactions), 1)
        transaction = subscription.transactions[0]
        self.assertEqual(transaction.external_id, 
                         'MOCK_PROCESSOR_TRANSACTION_ID')
        self.assertEqual(transaction.status, TransactionModel.STATUS_DONE)
Пример #34
0
    def test_cancel_subscription_with_prorated_refund(self):
        from billy.models.subscription import SubscriptionModel
        from billy.models.transaction import TransactionModel

        subscription_model = SubscriptionModel(self.testapp.session)
        tx_model = TransactionModel(self.testapp.session)
        now = datetime.datetime.utcnow()

        with db_transaction.manager:
            subscription_guid = subscription_model.create(
                customer_guid=self.customer_guid,
                plan_guid=self.plan_guid,
                amount=100,
            )
            tx_guid = tx_model.create(
                subscription_guid=subscription_guid,
                transaction_type=tx_model.TYPE_CHARGE,
                amount=100, 
                scheduled_at=now,
            )
            subscription = subscription_model.get(subscription_guid)
            subscription.period = 1
            subscription.next_transaction_at = datetime.datetime(2013, 8, 23)
            self.testapp.session.add(subscription)

            transaction = tx_model.get(tx_guid)
            transaction.status = tx_model.STATUS_DONE
            transaction.external_id = 'MOCK_BALANCED_DEBIT_URI'
            self.testapp.session.add(transaction)

        refund_called = []

        def mock_refund(transaction):
            refund_called.append(transaction)
            return 'MOCK_PROCESSOR_REFUND_URI'

        mock_processor = flexmock(DummyProcessor)
        (
            mock_processor
            .should_receive('refund')
            .replace_with(mock_refund)
            .once()
        )

        with freeze_time('2013-08-17'):
            canceled_at = datetime.datetime.utcnow()
            res = self.testapp.post(
                '/v1/subscriptions/{}/cancel'.format(subscription_guid), 
                dict(prorated_refund=True),
                extra_environ=dict(REMOTE_USER=self.api_key), 
                status=200,
            )

        subscription = res.json
        self.assertEqual(subscription['canceled'], True)
        self.assertEqual(subscription['canceled_at'], canceled_at.isoformat())

        transaction = refund_called[0]
        self.testapp.session.add(transaction)
        self.assertEqual(transaction.refund_to.guid, tx_guid)
        self.assertEqual(transaction.subscription_guid, subscription_guid)
        # only one day is elapsed, and it is a weekly plan, so
        # it should be 100 - (100 / 7) and round to cent, 85.71
        self.assertEqual(transaction.amount, decimal.Decimal('85.71'))
        self.assertEqual(transaction.status, tx_model.STATUS_DONE)

        res = self.testapp.get(
            '/v1/transactions', 
            extra_environ=dict(REMOTE_USER=self.api_key), 
            status=200,
        )
        guids = [item['guid'] for item in res.json['items']]
        self.assertEqual(set(guids), set([tx_guid, transaction.guid]))
Пример #35
0
    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)
Пример #36
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=int(transaction.amount * 100),
            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=int(transaction.amount * 100),
            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')
Пример #37
0
    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)
Пример #38
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]))
Пример #39
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')
Пример #40
0
    def test_cancel_subscription_with_refund_amount(self):
        from billy.models.subscription import SubscriptionModel
        from billy.models.transaction import TransactionModel

        subscription_model = SubscriptionModel(self.testapp.session)
        tx_model = TransactionModel(self.testapp.session)
        now = datetime.datetime.utcnow()

        with db_transaction.manager:
            subscription_guid = subscription_model.create(
                customer_guid=self.customer_guid,
                plan_guid=self.plan_guid,
            )
            tx_guid = tx_model.create(
                subscription_guid=subscription_guid,
                transaction_type=tx_model.TYPE_CHARGE,
                amount=10,
                scheduled_at=now,
            )
            subscription = subscription_model.get(subscription_guid)
            subscription.period = 1
            subscription.next_transaction_at = datetime.datetime(2013, 8, 23)
            self.testapp.session.add(subscription)

            transaction = tx_model.get(tx_guid)
            transaction.status = tx_model.STATUS_DONE
            transaction.external_id = 'MOCK_BALANCED_DEBIT_URI'
            self.testapp.session.add(transaction)

        refund_called = []

        def mock_refund(transaction):
            refund_called.append(transaction)
            return 'MOCK_PROCESSOR_REFUND_URI'

        mock_processor = flexmock(DummyProcessor)
        (mock_processor.should_receive('refund').replace_with(
            mock_refund).once())

        res = self.testapp.post(
            '/v1/subscriptions/{}/cancel'.format(subscription_guid),
            dict(refund_amount='2.34'),
            extra_environ=dict(REMOTE_USER=self.api_key),
            status=200,
        )
        subscription = res.json

        transaction = refund_called[0]
        self.testapp.session.add(transaction)
        self.assertEqual(transaction.refund_to.guid, tx_guid)
        self.assertEqual(transaction.subscription_guid, subscription_guid)
        self.assertEqual(transaction.amount, decimal.Decimal('2.34'))
        self.assertEqual(transaction.status, tx_model.STATUS_DONE)

        res = self.testapp.get(
            '/v1/transactions',
            extra_environ=dict(REMOTE_USER=self.api_key),
            status=200,
        )
        guids = [item['guid'] for item in res.json['items']]
        self.assertEqual(set(guids), set([tx_guid, transaction.guid]))