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')
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())
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, )
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 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)
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
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
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)
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
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, )
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
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]))
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
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, )
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)
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 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)
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, )
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())
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, )
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'))
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, )
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)
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()
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())
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')
def make_one(self, *args, **kwargs): from billy.models.subscription import SubscriptionModel return SubscriptionModel(*args, **kwargs)
def create_subscription_model(self): """Create a subscription model """ return SubscriptionModel(self)
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)
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]))
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)
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')
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)
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]))
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')
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]))