Esempio n. 1
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 
Esempio n. 2
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 
Esempio n. 3
0
def subscription_list_post(request):
    """Create a new subscription 

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

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

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

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

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

    subscription = model.get(guid)
    return subscription
Esempio n. 4
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)
Esempio n. 5
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
Esempio n. 6
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
Esempio n. 7
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]))
Esempio n. 8
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
Esempio n. 9
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)
Esempio n. 10
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'))
Esempio n. 11
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'))
Esempio n. 12
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())
Esempio n. 13
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)
Esempio n. 14
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]))
Esempio n. 15
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]))