Пример #1
0
    def create(
        self,
        company,
        plan_type,
        amount,
        frequency,
        interval=1,
        external_id=None,
        name=None,
        description=None,
    ):
        """Create a plan and return its ID

        """
        if interval < 1:
            raise ValueError('Interval can only be >= 1')
        now = tables.now_func()
        plan = tables.Plan(
            guid='PL' + make_guid(),
            company=company,
            plan_type=plan_type,
            amount=amount,
            frequency=frequency,
            interval=interval,
            external_id=external_id,
            name=name,
            description=description,
            updated_at=now,
            created_at=now,
        )
        self.session.add(plan)
        self.session.flush()
        return plan
Пример #2
0
    def create(
        self,
        company,
        processor_uri=None,
    ):
        """Create a customer and return its id

        """
        now = tables.now_func()
        customer = tables.Customer(
            guid='CU' + make_guid(),
            company=company,
            processor_uri=processor_uri,
            created_at=now,
            updated_at=now,
        )
        self.session.add(customer)
        self.session.flush()

        processor = self.factory.create_processor()
        processor.configure_api_key(customer.company.processor_key)
        # create customer
        if customer.processor_uri is None:
            customer.processor_uri = processor.create_customer(customer)
        # validate the customer processor URI
        else:
            processor.validate_customer(customer.processor_uri)

        self.session.flush()
        return customer
Пример #3
0
    def create(
        self,
        company,
        processor_uri=None,
    ):
        """Create a customer and return its id

        """
        now = tables.now_func()
        customer = tables.Customer(
            guid='CU' + make_guid(),
            company=company,
            processor_uri=processor_uri,
            created_at=now,
            updated_at=now,
        )
        self.session.add(customer)
        self.session.flush()

        processor = self.factory.create_processor()
        processor.configure_api_key(customer.company.processor_key)
        # create customer
        if customer.processor_uri is None:
            customer.processor_uri = processor.create_customer(customer)
        # validate the customer processor URI
        else:
            processor.validate_customer(customer.processor_uri)

        self.session.flush()
        return customer
Пример #4
0
    def create(
        self, company_guid, plan_type, amount, frequency, interval=1, external_id=None, name=None, description=None
    ):
        """Create a plan and return its ID

        """
        if plan_type not in self.TYPE_ALL:
            raise ValueError("Invalid plan_type {}".format(plan_type))
        if frequency not in self.FREQ_ALL:
            raise ValueError("Invalid frequency {}".format(frequency))
        if interval < 1:
            raise ValueError("Interval can only be >= 1")
        now = tables.now_func()
        plan = tables.Plan(
            guid="PL" + make_guid(),
            company_guid=company_guid,
            plan_type=plan_type,
            amount=amount,
            frequency=frequency,
            interval=interval,
            external_id=external_id,
            name=name,
            description=description,
            updated_at=now,
            created_at=now,
        )
        self.session.add(plan)
        self.session.flush()
        return plan.guid
Пример #5
0
    def create(
        self, 
        customer_guid, 
        plan_guid, 
        payment_uri=None, 
        started_at=None,
        external_id=None,
        amount=None,
    ):
        """Create a subscription and return its id

        """
        if amount is not None and amount <= 0:
            raise ValueError('Amount should be a non-zero postive float number')
        now = tables.now_func()
        if started_at is None:
            started_at = now
        elif started_at < now:
            raise ValueError('Past started_at time is not allowed')
        subscription = tables.Subscription(
            guid='SU' + make_guid(),
            customer_guid=customer_guid,
            plan_guid=plan_guid,
            amount=amount, 
            payment_uri=payment_uri, 
            external_id=external_id, 
            started_at=started_at, 
            next_transaction_at=started_at, 
            created_at=now,
            updated_at=now,
        )
        self.session.add(subscription)
        self.session.flush()
        return subscription.guid
Пример #6
0
    def add_event(self, transaction, status, processor_id, occurred_at):
        """Add a status updating event of transaction from callback

        """
        now = tables.now_func()
        # the latest event of this transaction
        last_event = transaction.events.first()

        event = tables.TransactionEvent(
            guid="TE" + make_guid(),
            transaction=transaction,
            processor_id=processor_id,
            occurred_at=occurred_at,
            status=status,
            created_at=now,
        )
        self.session.add(event)

        # ensure won't duplicate
        try:
            self.session.flush()
        except IntegrityError:
            self.session.rollback()
            raise DuplicateEventError("Event {} already exists for {}".format(processor_id, transaction.guid))

        # Notice: we only want to update transaction status if this event
        # is the latest one we had seen in Billy system. Why we are doing
        # here is because I think of some scenarios like
        #
        #  1. Balanced cannot reach Billy for a short while, and retry later
        #  2. Attacker want to fool us with old events
        #
        # These will lead the status of invoice to be updated incorrectly.
        # For case 1, events send to Billy like this:
        #
        #     succeeded (failed to send to Billy, retry 1 minute later)
        #     failed
        #     succeeded (retry)
        #
        # See? The final status should be `failed`, but as the `succeeded`
        # was resent later, so it became `succeded` eventually. Similarly,
        # attackers can send us an old `succeeded` event to make the invoice
        # settled.  This is why we need to ensure only the latest event can
        # affect status of invoice.
        if last_event is not None and occurred_at <= last_event.occurred_at:
            return

        old_status = transaction.status
        transaction.updated_at = now
        transaction.status = status
        # update invoice status
        invoice_model = self.factory.create_invoice_model()
        invoice_model.transaction_status_update(
            invoice=transaction.invoice, transaction=transaction, original_status=old_status
        )
        self.session.flush()
Пример #7
0
    def create(
        self,
        invoice,
        amount,
        transaction_type=None,
        funding_instrument_uri=None,
        reference_to=None,
        appears_on_statement_as=None,
    ):
        """Create a transaction and return

        """
        if transaction_type is None:
            transaction_type = invoice.transaction_type

        if transaction_type not in self.TYPE_ALL:
            raise ValueError('Invalid transaction_type {}'
                             .format(transaction_type))
        if reference_to is not None:
            if transaction_type not in [self.TYPE_REFUND, self.TYPE_REVERSE]:
                raise ValueError('reference_to can only be set to a refund '
                                 'transaction')
            if funding_instrument_uri is not None:
                raise ValueError(
                    'funding_instrument_uri cannot be set to a refund/reverse '
                    'transaction'
                )
            if (
                reference_to.transaction_type not in
                [self.TYPE_CHARGE, self.TYPE_PAYOUT]
            ):
                raise ValueError(
                    'Only charge/payout transaction can be refunded/reversed'
                )

        now = tables.now_func()
        transaction = tables.Transaction(
            guid='TX' + make_guid(),
            transaction_type=transaction_type,
            amount=amount,
            funding_instrument_uri=funding_instrument_uri,
            appears_on_statement_as=appears_on_statement_as,
            status=self.STATUS_INIT,
            reference_to=reference_to,
            created_at=now,
            updated_at=now,
            invoice=invoice,
        )
        self.session.add(transaction)
        self.session.flush()
        return transaction
Пример #8
0
    def create(self, transaction, error_message, error_code=None, error_number=None):
        """Create a failure for and return

        """
        failure = self.TABLE(
            guid="TF" + make_guid(),
            transaction=transaction,
            error_message=error_message,
            error_code=error_code,
            error_number=error_number,
        )
        self.session.add(failure)
        self.session.flush()
        return failure
Пример #9
0
    def create(self, company_guid, external_id=None):
        """Create a customer and return its id

        """
        now = tables.now_func()
        customer = tables.Customer(
            guid='CU' + make_guid(),
            company_guid=company_guid,
            external_id=external_id,
            created_at=now,
            updated_at=now,
        )
        self.session.add(customer)
        self.session.flush()
        return customer.guid
Пример #10
0
    def create(self, processor_key, name=None):
        """Create a company and return its id

        """
        now = tables.now_func()
        company = tables.Company(
            guid='CP' + make_guid(),
            processor_key=processor_key,
            api_key=make_api_key(),
            name=name, 
            created_at=now,
            updated_at=now,
        )
        self.session.add(company)
        self.session.flush()
        return company.guid
Пример #11
0
    def create(self, processor_key, name=None):
        """Create a company and return

        """
        now = tables.now_func()
        company = tables.Company(
            guid='CP' + make_guid(),
            processor_key=processor_key,
            api_key=make_api_key(),
            name=name,
            created_at=now,
            updated_at=now,
        )
        self.session.add(company)
        self.session.flush()
        return company
Пример #12
0
    def create(
        self,
        invoice,
        amount,
        transaction_type=None,
        funding_instrument_uri=None,
        reference_to=None,
        appears_on_statement_as=None,
    ):
        """Create a transaction and return

        """
        if transaction_type is None:
            transaction_type = invoice.transaction_type

        if reference_to is not None:
            if transaction_type not in [self.types.REFUND, self.types.REVERSE]:
                raise ValueError('reference_to can only be set to a refund '
                                 'transaction')
            if funding_instrument_uri is not None:
                raise ValueError(
                    'funding_instrument_uri cannot be set to a refund/reverse '
                    'transaction')
            if (reference_to.transaction_type
                    not in [self.types.DEBIT, self.types.CREDIT]):
                raise ValueError(
                    'Only charge/payout transaction can be refunded/reversed')

        now = tables.now_func()
        transaction = tables.Transaction(
            guid='TX' + make_guid(),
            transaction_type=transaction_type,
            amount=amount,
            funding_instrument_uri=funding_instrument_uri,
            appears_on_statement_as=appears_on_statement_as,
            submit_status=self.submit_statuses.STAGED,
            reference_to=reference_to,
            created_at=now,
            updated_at=now,
            invoice=invoice,
        )
        self.session.add(transaction)
        self.session.flush()
        return transaction
Пример #13
0
    def create(
        self, 
        company_guid, 
        external_id=None
    ):
        """Create a customer and return its id

        """
        now = tables.now_func()
        customer = tables.Customer(
            guid='CU' + make_guid(),
            company_guid=company_guid,
            external_id=external_id, 
            created_at=now,
            updated_at=now,
        )
        self.session.add(customer)
        self.session.flush()
        return customer.guid
Пример #14
0
    def create(
        self, 
        subscription_guid, 
        transaction_type, 
        amount,
        scheduled_at,
        payment_uri=None,
        refund_to_guid=None,
    ):
        """Create a transaction and return its ID

        """
        if transaction_type not in self.TYPE_ALL:
            raise ValueError('Invalid transaction_type {}'
                             .format(transaction_type))
        if refund_to_guid is not None:
            if transaction_type != self.TYPE_REFUND:
                raise ValueError('refund_to_guid can only be set to a refund '
                                 'transaction')
            if payment_uri is not None:
                raise ValueError('payment_uri cannot be set to a refund '
                                 'transaction')
            refund_transaction = self.get(refund_to_guid, raise_error=True)
            if refund_transaction.transaction_type != self.TYPE_CHARGE:
                raise ValueError('Only charge transaction can be refunded')

        now = tables.now_func()
        transaction = tables.Transaction(
            guid='TX' + make_guid(),
            subscription_guid=subscription_guid,
            transaction_type=transaction_type,
            amount=amount, 
            payment_uri=payment_uri, 
            status=self.STATUS_INIT, 
            scheduled_at=scheduled_at, 
            refund_to_guid=refund_to_guid, 
            created_at=now, 
            updated_at=now, 
        )
        self.session.add(transaction)
        self.session.flush()
        return transaction.guid
Пример #15
0
    def create(
        self,
        subscription_guid,
        transaction_type,
        amount,
        scheduled_at,
        payment_uri=None,
        refund_to_guid=None,
    ):
        """Create a transaction and return its ID

        """
        if transaction_type not in self.TYPE_ALL:
            raise ValueError(
                'Invalid transaction_type {}'.format(transaction_type))
        if refund_to_guid is not None:
            if transaction_type != self.TYPE_REFUND:
                raise ValueError('refund_to_guid can only be set to a refund '
                                 'transaction')
            if payment_uri is not None:
                raise ValueError('payment_uri cannot be set to a refund '
                                 'transaction')
            refund_transaction = self.get(refund_to_guid, raise_error=True)
            if refund_transaction.transaction_type != self.TYPE_CHARGE:
                raise ValueError('Only charge transaction can be refunded')

        now = tables.now_func()
        transaction = tables.Transaction(
            guid='TX' + make_guid(),
            subscription_guid=subscription_guid,
            transaction_type=transaction_type,
            amount=amount,
            payment_uri=payment_uri,
            status=self.STATUS_INIT,
            scheduled_at=scheduled_at,
            refund_to_guid=refund_to_guid,
            created_at=now,
            updated_at=now,
        )
        self.session.add(transaction)
        self.session.flush()
        return transaction.guid
Пример #16
0
    def create(
        self,
        transaction,
        error_message,
        error_code=None,
        error_number=None,
    ):
        """Create a failure for and return

        """
        failure = self.TABLE(
            guid='TF' + make_guid(),
            transaction=transaction,
            error_message=error_message,
            error_code=error_code,
            error_number=error_number,
        )
        self.session.add(failure)
        self.session.flush()
        return failure
Пример #17
0
    def create(
        self,
        invoice,
        amount,
        transaction_type=None,
        funding_instrument_uri=None,
        reference_to=None,
        appears_on_statement_as=None,
    ):
        """Create a transaction and return

        """
        if transaction_type is None:
            transaction_type = invoice.transaction_type

        if reference_to is not None:
            if transaction_type not in [self.types.REFUND, self.types.REVERSE]:
                raise ValueError("reference_to can only be set to a refund " "transaction")
            if funding_instrument_uri is not None:
                raise ValueError("funding_instrument_uri cannot be set to a refund/reverse " "transaction")
            if reference_to.transaction_type not in [self.types.DEBIT, self.types.CREDIT]:
                raise ValueError("Only charge/payout transaction can be refunded/reversed")

        now = tables.now_func()
        transaction = tables.Transaction(
            guid="TX" + make_guid(),
            transaction_type=transaction_type,
            amount=amount,
            funding_instrument_uri=funding_instrument_uri,
            appears_on_statement_as=appears_on_statement_as,
            submit_status=self.submit_statuses.STAGED,
            reference_to=reference_to,
            created_at=now,
            updated_at=now,
            invoice=invoice,
        )
        self.session.add(transaction)
        self.session.flush()
        return transaction
Пример #18
0
    def create(self, company, plan_type, amount, frequency, interval=1, external_id=None, name=None, description=None):
        """Create a plan and return its ID

        """
        if interval < 1:
            raise ValueError("Interval can only be >= 1")
        now = tables.now_func()
        plan = tables.Plan(
            guid="PL" + make_guid(),
            company=company,
            plan_type=plan_type,
            amount=amount,
            frequency=frequency,
            interval=interval,
            external_id=external_id,
            name=name,
            description=description,
            updated_at=now,
            created_at=now,
        )
        self.session.add(plan)
        self.session.flush()
        return plan
Пример #19
0
    def create(
        self,
        customer,
        plan,
        funding_instrument_uri=None,
        started_at=None,
        external_id=None,
        appears_on_statement_as=None,
        amount=None,
    ):
        """Create a subscription and return its id

        """
        if amount is not None and amount <= 0:
            raise ValueError('Amount should be a non-zero postive integer')
        now = tables.now_func()
        if started_at is None:
            started_at = now
        elif started_at < now:
            raise ValueError('Past started_at time is not allowed')
        subscription = tables.Subscription(
            guid='SU' + make_guid(),
            customer=customer,
            plan=plan,
            amount=amount,
            funding_instrument_uri=funding_instrument_uri,
            external_id=external_id,
            appears_on_statement_as=appears_on_statement_as,
            started_at=started_at,
            next_invoice_at=started_at,
            created_at=now,
            updated_at=now,
        )
        self.session.add(subscription)
        self.session.flush()
        self.yield_invoices([subscription])
        return subscription
Пример #20
0
    def create(
        self,
        customer,
        plan,
        funding_instrument_uri=None,
        started_at=None,
        external_id=None,
        appears_on_statement_as=None,
        amount=None,
    ):
        """Create a subscription and return its id

        """
        if amount is not None and amount <= 0:
            raise ValueError("Amount should be a non-zero postive integer")
        now = tables.now_func()
        if started_at is None:
            started_at = now
        elif started_at < now:
            raise ValueError("Past started_at time is not allowed")
        subscription = tables.Subscription(
            guid="SU" + make_guid(),
            customer=customer,
            plan=plan,
            amount=amount,
            funding_instrument_uri=funding_instrument_uri,
            external_id=external_id,
            appears_on_statement_as=appears_on_statement_as,
            started_at=started_at,
            next_invoice_at=started_at,
            created_at=now,
            updated_at=now,
        )
        self.session.add(subscription)
        self.session.flush()
        self.yield_invoices([subscription])
        return subscription
Пример #21
0
    def create(
        self,
        company_guid,
        plan_type,
        amount,
        frequency,
        interval=1,
        external_id=None,
        name=None,
        description=None,
    ):
        """Create a plan and return its ID

        """
        if plan_type not in self.TYPE_ALL:
            raise ValueError('Invalid plan_type {}'.format(plan_type))
        if frequency not in self.FREQ_ALL:
            raise ValueError('Invalid frequency {}'.format(frequency))
        if interval < 1:
            raise ValueError('Interval can only be >= 1')
        now = tables.now_func()
        plan = tables.Plan(
            guid='PL' + make_guid(),
            company_guid=company_guid,
            plan_type=plan_type,
            amount=amount,
            frequency=frequency,
            interval=interval,
            external_id=external_id,
            name=name,
            description=description,
            updated_at=now,
            created_at=now,
        )
        self.session.add(plan)
        self.session.flush()
        return plan.guid
Пример #22
0
    def create(self, processor_key, name=None, make_callback_url=None):
        """Create a company and return

        """
        now = tables.now_func()
        company = tables.Company(
            guid='CP' + make_guid(),
            processor_key=processor_key,
            api_key=make_api_key(),
            callback_key=make_api_key(),
            name=name,
            created_at=now,
            updated_at=now,
        )
        self.session.add(company)
        self.session.flush()

        if make_callback_url is not None:
            url = make_callback_url(company)
            processor = self.factory.create_processor()
            processor.configure_api_key(company.processor_key)
            processor.register_callback(company, url)

        return company
Пример #23
0
    def add_event(self, transaction, status, processor_id, occurred_at):
        """Add a status updating event of transaction from callback

        """
        now = tables.now_func()
        # the latest event of this transaction
        last_event = transaction.events.first()

        event = tables.TransactionEvent(
            guid='TE' + make_guid(),
            transaction=transaction,
            processor_id=processor_id,
            occurred_at=occurred_at,
            status=status,
            created_at=now,
        )
        self.session.add(event)

        # ensure won't duplicate
        try:
            self.session.flush()
        except IntegrityError:
            self.session.rollback()
            raise DuplicateEventError(
                'Event {} already exists for {}'.format(
                    processor_id,
                    transaction.guid,
                ), )

        # Notice: we only want to update transaction status if this event
        # is the latest one we had seen in Billy system. Why we are doing
        # here is because I think of some scenarios like
        #
        #  1. Balanced cannot reach Billy for a short while, and retry later
        #  2. Attacker want to fool us with old events
        #
        # These will lead the status of invoice to be updated incorrectly.
        # For case 1, events send to Billy like this:
        #
        #     succeeded (failed to send to Billy, retry 1 minute later)
        #     failed
        #     succeeded (retry)
        #
        # See? The final status should be `failed`, but as the `succeeded`
        # was resent later, so it became `succeded` eventually. Similarly,
        # attackers can send us an old `succeeded` event to make the invoice
        # settled.  This is why we need to ensure only the latest event can
        # affect status of invoice.
        if last_event is not None and occurred_at <= last_event.occurred_at:
            return

        old_status = transaction.status
        transaction.updated_at = now
        transaction.status = status
        # update invoice status
        invoice_model = self.factory.create_invoice_model()
        invoice_model.transaction_status_update(
            invoice=transaction.invoice,
            transaction=transaction,
            original_status=old_status,
        )
        self.session.flush()
Пример #24
0
    def test_make_guid(self):
        from billy.utils.generic import make_guid

        # just make sure it is random
        guids = [make_guid() for _ in range(100)]
        self.assertEqual(len(set(guids)), 100)
Пример #25
0
    def test_make_guid(self):
        from billy.utils.generic import make_guid

        # just make sure it is random
        guids = [make_guid() for _ in range(100)]
        self.assertEqual(len(set(guids)), 100)
Пример #26
0
 def test_make_guid(self):
     # just make sure it is random
     guids = [make_guid() for _ in range(100)]
     self.assertEqual(len(set(guids)), 100)
Пример #27
0
 def test_make_guid(self):
     # just make sure it is random
     guids = [make_guid() for _ in range(100)]
     self.assertEqual(len(set(guids)), 100)
Пример #28
0
    def create(
        self,
        amount,
        funding_instrument_uri=None,
        customer=None,
        subscription=None,
        title=None,
        items=None,
        adjustments=None,
        external_id=None,
        appears_on_statement_as=None,
        scheduled_at=None,
    ):
        """Create a invoice and return its id

        """
        from sqlalchemy.exc import IntegrityError

        if customer is not None and subscription is not None:
            raise ValueError('You can only set either customer or subscription')

        if customer is not None:
            invoice_type = self.types.CUSTOMER
            invoice_cls = tables.CustomerInvoice
            # we only support charge type for customer invoice now
            transaction_type = self.transaction_types.DEBIT
            extra_kwargs = dict(
                customer=customer,
                external_id=external_id,
            )
        elif subscription is not None:
            if scheduled_at is None:
                raise ValueError('scheduled_at cannot be None')
            invoice_type = self.types.SUBSCRIPTION
            invoice_cls = tables.SubscriptionInvoice
            plan_type = subscription.plan.plan_type
            if plan_type == PlanModel.types.DEBIT:
                transaction_type = self.transaction_types.DEBIT
            elif plan_type == PlanModel.types.CREDIT:
                transaction_type = self.transaction_types.CREDIT
            else:
                raise ValueError('Invalid plan_type {}'.format(plan_type))
            extra_kwargs = dict(
                subscription=subscription,
                scheduled_at=scheduled_at,
            )
        else:
            raise ValueError('You have to set either customer or subscription')

        if amount < 0:
            raise ValueError('Negative amount {} is not allowed'.format(amount))

        now = tables.now_func()
        invoice = invoice_cls(
            guid='IV' + make_guid(),
            invoice_type=invoice_type,
            transaction_type=transaction_type,
            status=self.statuses.STAGED,
            amount=amount,
            funding_instrument_uri=funding_instrument_uri,
            title=title,
            created_at=now,
            updated_at=now,
            appears_on_statement_as=appears_on_statement_as,
            **extra_kwargs
        )

        self.session.add(invoice)

        # ensure (customer_guid, external_id) is unique
        try:
            self.session.flush()
        except IntegrityError:
            self.session.rollback()
            raise DuplicateExternalIDError(
                'Invoice {} with external_id {} already exists'
                .format(customer.guid, external_id)
            )

        if items:
            for item in items:
                record = tables.Item(
                    invoice=invoice,
                    name=item['name'],
                    amount=item['amount'],
                    type=item.get('type'),
                    quantity=item.get('quantity'),
                    unit=item.get('unit'),
                    volume=item.get('volume'),
                )
                self.session.add(record)
            self.session.flush()

        # TODO: what about an invalid adjust? say, it makes the total of invoice
        # a negative value? I think we should not allow user to create such
        # invalid invoice
        if adjustments:
            for adjustment in adjustments:
                record = tables.Adjustment(
                    invoice=invoice,
                    amount=adjustment['amount'],
                    reason=adjustment.get('reason'),
                )
                self.session.add(record)
            self.session.flush()

        # as if we set the funding_instrument_uri at very first, we want to charge it
        # immediately, so we create a transaction right away, also set the
        # status to PROCESSING
        if funding_instrument_uri is not None and invoice.amount > 0:
            invoice.status = self.statuses.PROCESSING
            self._create_transaction(invoice)
        # it is zero amount, nothing to charge, just switch to
        # SETTLED status
        elif invoice.amount == 0:
            invoice.status = self.statuses.SETTLED

        self.session.flush()
        return invoice