예제 #1
0
파일: stripe.py 프로젝트: raghupalash/zulip
def do_change_plan_status(plan: CustomerPlan, status: int) -> None:
    plan.status = status
    plan.save(update_fields=["status"])
    billing_logger.info(
        "Change plan status: Customer.id: %s, CustomerPlan.id: %s, status: %s",
        plan.customer.id,
        plan.id,
        status,
    )
예제 #2
0
def process_downgrade(plan: CustomerPlan) -> None:
    from zerver.lib.actions import do_change_plan_type

    assert plan.customer.realm is not None
    do_change_plan_type(plan.customer.realm,
                        Realm.PLAN_TYPE_LIMITED,
                        acting_user=None)
    plan.status = CustomerPlan.ENDED
    plan.save(update_fields=["status"])
예제 #3
0
파일: stripe.py 프로젝트: vineetb95/zulip
def make_end_of_cycle_updates_if_needed(
        plan: CustomerPlan, event_time: datetime) -> Optional[LicenseLedger]:
    last_ledger_entry = LicenseLedger.objects.filter(
        plan=plan).order_by('-id').first()
    last_renewal = LicenseLedger.objects.filter(plan=plan, is_renewal=True) \
                                        .order_by('-id').first().event_time
    next_billing_cycle = start_of_next_billing_cycle(plan, last_renewal)
    if next_billing_cycle <= event_time:
        if plan.status == CustomerPlan.ACTIVE:
            return LicenseLedger.objects.create(
                plan=plan,
                is_renewal=True,
                event_time=next_billing_cycle,
                licenses=last_ledger_entry.licenses_at_next_renewal,
                licenses_at_next_renewal=last_ledger_entry.
                licenses_at_next_renewal)
        if plan.status == CustomerPlan.FREE_TRIAL:
            plan.invoiced_through = last_ledger_entry
            assert (plan.next_invoice_date is not None)
            plan.billing_cycle_anchor = plan.next_invoice_date.replace(
                microsecond=0)
            plan.status = CustomerPlan.ACTIVE
            plan.save(update_fields=[
                "invoiced_through", "billing_cycle_anchor", "status"
            ])
            return LicenseLedger.objects.create(
                plan=plan,
                is_renewal=True,
                event_time=next_billing_cycle,
                licenses=last_ledger_entry.licenses_at_next_renewal,
                licenses_at_next_renewal=last_ledger_entry.
                licenses_at_next_renewal)
        if plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE:
            process_downgrade(plan)
        return None
    return last_ledger_entry
예제 #4
0
파일: stripe.py 프로젝트: vineetb95/zulip
def process_downgrade(plan: CustomerPlan) -> None:
    from zerver.lib.actions import do_change_plan_type
    do_change_plan_type(plan.customer.realm, Realm.LIMITED)
    plan.status = CustomerPlan.ENDED
    plan.save(update_fields=['status'])
예제 #5
0
파일: stripe.py 프로젝트: vineetb95/zulip
def invoice_plan(plan: CustomerPlan, event_time: datetime) -> None:
    if plan.invoicing_status == CustomerPlan.STARTED:
        raise NotImplementedError(
            'Plan with invoicing_status==STARTED needs manual resolution.')
    make_end_of_cycle_updates_if_needed(plan, event_time)
    assert (plan.invoiced_through is not None)
    licenses_base = plan.invoiced_through.licenses
    invoice_item_created = False
    for ledger_entry in LicenseLedger.objects.filter(
            plan=plan, id__gt=plan.invoiced_through.id,
            event_time__lte=event_time).order_by('id'):
        price_args: Dict[str, int] = {}
        if ledger_entry.is_renewal:
            if plan.fixed_price is not None:
                price_args = {'amount': plan.fixed_price}
            else:
                assert (plan.price_per_license is not None)  # needed for mypy
                price_args = {
                    'unit_amount': plan.price_per_license,
                    'quantity': ledger_entry.licenses
                }
            description = "Zulip Standard - renewal"
        elif ledger_entry.licenses != licenses_base:
            assert (plan.price_per_license)
            last_renewal = LicenseLedger.objects.filter(
                plan=plan, is_renewal=True, event_time__lte=ledger_entry.event_time) \
                .order_by('-id').first().event_time
            period_end = start_of_next_billing_cycle(plan,
                                                     ledger_entry.event_time)
            proration_fraction = (period_end - ledger_entry.event_time) / (
                period_end - last_renewal)
            price_args = {
                'unit_amount':
                int(plan.price_per_license * proration_fraction + .5),
                'quantity':
                ledger_entry.licenses - licenses_base
            }
            description = "Additional license ({} - {})".format(
                ledger_entry.event_time.strftime('%b %-d, %Y'),
                period_end.strftime('%b %-d, %Y'))

        if price_args:
            plan.invoiced_through = ledger_entry
            plan.invoicing_status = CustomerPlan.STARTED
            plan.save(update_fields=['invoicing_status', 'invoiced_through'])
            idempotency_key: Optional[str] = 'ledger_entry:{}'.format(
                ledger_entry.id)
            if settings.TEST_SUITE:
                idempotency_key = None
            stripe.InvoiceItem.create(
                currency='usd',
                customer=plan.customer.stripe_customer_id,
                description=description,
                discountable=False,
                period={
                    'start':
                    datetime_to_timestamp(ledger_entry.event_time),
                    'end':
                    datetime_to_timestamp(
                        start_of_next_billing_cycle(plan,
                                                    ledger_entry.event_time))
                },
                idempotency_key=idempotency_key,
                **price_args)
            invoice_item_created = True
        plan.invoiced_through = ledger_entry
        plan.invoicing_status = CustomerPlan.DONE
        plan.save(update_fields=['invoicing_status', 'invoiced_through'])
        licenses_base = ledger_entry.licenses

    if invoice_item_created:
        if plan.charge_automatically:
            billing_method = 'charge_automatically'
            days_until_due = None
        else:
            billing_method = 'send_invoice'
            days_until_due = DEFAULT_INVOICE_DAYS_UNTIL_DUE
        stripe_invoice = stripe.Invoice.create(
            auto_advance=True,
            billing=billing_method,
            customer=plan.customer.stripe_customer_id,
            days_until_due=days_until_due,
            statement_descriptor='Zulip Standard')
        stripe.Invoice.finalize_invoice(stripe_invoice)

    plan.next_invoice_date = next_invoice_date(plan)
    plan.save(update_fields=['next_invoice_date'])
예제 #6
0
파일: stripe.py 프로젝트: jdherg/zulip
def invoice_plan(plan: CustomerPlan, event_time: datetime) -> None:
    if plan.invoicing_status == CustomerPlan.STARTED:
        raise NotImplementedError('Plan with invoicing_status==STARTED needs manual resolution.')
    add_plan_renewal_to_license_ledger_if_needed(plan, event_time)
    assert(plan.invoiced_through is not None)
    licenses_base = plan.invoiced_through.licenses
    invoice_item_created = False
    for ledger_entry in LicenseLedger.objects.filter(plan=plan, id__gt=plan.invoiced_through.id,
                                                     event_time__lte=event_time).order_by('id'):
        price_args = {}  # type: Dict[str, int]
        if ledger_entry.is_renewal:
            if plan.fixed_price is not None:
                price_args = {'amount': plan.fixed_price}
            else:
                assert(plan.price_per_license is not None)  # needed for mypy
                price_args = {'unit_amount': plan.price_per_license,
                              'quantity': ledger_entry.licenses}
            description = "Zulip Standard - renewal"
        elif ledger_entry.licenses != licenses_base:
            assert(plan.price_per_license)
            last_renewal = LicenseLedger.objects.filter(
                plan=plan, is_renewal=True, event_time__lte=ledger_entry.event_time) \
                .order_by('-id').first().event_time
            period_end = next_renewal_date(plan, ledger_entry.event_time)
            proration_fraction = (period_end - ledger_entry.event_time) / (period_end - last_renewal)
            price_args = {'unit_amount': int(plan.price_per_license * proration_fraction + .5),
                          'quantity': ledger_entry.licenses - licenses_base}
            description = "Additional license ({} - {})".format(
                ledger_entry.event_time.strftime('%b %-d, %Y'), period_end.strftime('%b %-d, %Y'))

        if price_args:
            plan.invoiced_through = ledger_entry
            plan.invoicing_status = CustomerPlan.STARTED
            plan.save(update_fields=['invoicing_status', 'invoiced_through'])
            idempotency_key = 'ledger_entry:{}'.format(ledger_entry.id)  # type: Optional[str]
            if settings.TEST_SUITE:
                idempotency_key = None
            stripe.InvoiceItem.create(
                currency='usd',
                customer=plan.customer.stripe_customer_id,
                description=description,
                discountable=False,
                period = {'start': datetime_to_timestamp(ledger_entry.event_time),
                          'end': datetime_to_timestamp(next_renewal_date(plan, ledger_entry.event_time))},
                idempotency_key=idempotency_key,
                **price_args)
            invoice_item_created = True
        plan.invoiced_through = ledger_entry
        plan.invoicing_status = CustomerPlan.DONE
        plan.save(update_fields=['invoicing_status', 'invoiced_through'])
        licenses_base = ledger_entry.licenses

    if invoice_item_created:
        if plan.charge_automatically:
            billing_method = 'charge_automatically'
            days_until_due = None
        else:
            billing_method = 'send_invoice'
            days_until_due = DEFAULT_INVOICE_DAYS_UNTIL_DUE
        stripe_invoice = stripe.Invoice.create(
            auto_advance=True,
            billing=billing_method,
            customer=plan.customer.stripe_customer_id,
            days_until_due=days_until_due,
            statement_descriptor='Zulip Standard')
        stripe.Invoice.finalize_invoice(stripe_invoice)

    plan.next_invoice_date = next_invoice_date(plan)
    plan.save(update_fields=['next_invoice_date'])
예제 #7
0
def make_end_of_cycle_updates_if_needed(
    plan: CustomerPlan, event_time: datetime
) -> Tuple[Optional[CustomerPlan], Optional[LicenseLedger]]:
    last_ledger_entry = LicenseLedger.objects.filter(
        plan=plan).order_by('-id').first()
    last_renewal = LicenseLedger.objects.filter(plan=plan, is_renewal=True) \
                                        .order_by('-id').first().event_time
    next_billing_cycle = start_of_next_billing_cycle(plan, last_renewal)
    if next_billing_cycle <= event_time:
        if plan.status == CustomerPlan.ACTIVE:
            return None, LicenseLedger.objects.create(
                plan=plan,
                is_renewal=True,
                event_time=next_billing_cycle,
                licenses=last_ledger_entry.licenses_at_next_renewal,
                licenses_at_next_renewal=last_ledger_entry.
                licenses_at_next_renewal)
        if plan.status == CustomerPlan.FREE_TRIAL:
            plan.invoiced_through = last_ledger_entry
            assert (plan.next_invoice_date is not None)
            plan.billing_cycle_anchor = plan.next_invoice_date.replace(
                microsecond=0)
            plan.status = CustomerPlan.ACTIVE
            plan.save(update_fields=[
                "invoiced_through", "billing_cycle_anchor", "status"
            ])
            return None, LicenseLedger.objects.create(
                plan=plan,
                is_renewal=True,
                event_time=next_billing_cycle,
                licenses=last_ledger_entry.licenses_at_next_renewal,
                licenses_at_next_renewal=last_ledger_entry.
                licenses_at_next_renewal)

        if plan.status == CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE:
            if plan.fixed_price is not None:  # nocoverage
                raise NotImplementedError(
                    "Can't switch fixed priced monthly plan to annual.")

            plan.status = CustomerPlan.ENDED
            plan.save(update_fields=["status"])

            discount = plan.customer.default_discount or plan.discount
            _, _, _, price_per_license = compute_plan_parameters(
                automanage_licenses=plan.automanage_licenses,
                billing_schedule=CustomerPlan.ANNUAL,
                discount=plan.discount)

            new_plan = CustomerPlan.objects.create(
                customer=plan.customer,
                billing_schedule=CustomerPlan.ANNUAL,
                automanage_licenses=plan.automanage_licenses,
                charge_automatically=plan.charge_automatically,
                price_per_license=price_per_license,
                discount=discount,
                billing_cycle_anchor=next_billing_cycle,
                tier=plan.tier,
                status=CustomerPlan.ACTIVE,
                next_invoice_date=next_billing_cycle,
                invoiced_through=None,
                invoicing_status=CustomerPlan.INITIAL_INVOICE_TO_BE_SENT,
            )

            new_plan_ledger_entry = LicenseLedger.objects.create(
                plan=new_plan,
                is_renewal=True,
                event_time=next_billing_cycle,
                licenses=last_ledger_entry.licenses_at_next_renewal,
                licenses_at_next_renewal=last_ledger_entry.
                licenses_at_next_renewal)

            RealmAuditLog.objects.create(
                realm=new_plan.customer.realm,
                event_time=event_time,
                event_type=RealmAuditLog.
                CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN,
                extra_data=orjson.dumps({
                    "monthly_plan_id": plan.id,
                    "annual_plan_id": new_plan.id,
                }).decode())
            return new_plan, new_plan_ledger_entry

        if plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE:
            process_downgrade(plan)
        return None, None
    return None, last_ledger_entry
예제 #8
0
def invoice_plan(plan: CustomerPlan, event_time: datetime) -> None:
    if plan.invoicing_status == CustomerPlan.STARTED:
        raise NotImplementedError("Plan with invoicing_status==STARTED needs manual resolution.")
    if not plan.customer.stripe_customer_id:
        assert plan.customer.realm is not None
        raise BillingError(
            f"Realm {plan.customer.realm.string_id} has a paid plan without a Stripe customer."
        )

    make_end_of_cycle_updates_if_needed(plan, event_time)

    if plan.invoicing_status == CustomerPlan.INITIAL_INVOICE_TO_BE_SENT:
        invoiced_through_id = -1
        licenses_base = None
    else:
        assert plan.invoiced_through is not None
        licenses_base = plan.invoiced_through.licenses
        invoiced_through_id = plan.invoiced_through.id

    invoice_item_created = False
    for ledger_entry in LicenseLedger.objects.filter(
        plan=plan, id__gt=invoiced_through_id, event_time__lte=event_time
    ).order_by("id"):
        price_args: Dict[str, int] = {}
        if ledger_entry.is_renewal:
            if plan.fixed_price is not None:
                price_args = {"amount": plan.fixed_price}
            else:
                assert plan.price_per_license is not None  # needed for mypy
                price_args = {
                    "unit_amount": plan.price_per_license,
                    "quantity": ledger_entry.licenses,
                }
            description = f"{plan.name} - renewal"
        elif licenses_base is not None and ledger_entry.licenses != licenses_base:
            assert plan.price_per_license
            last_ledger_entry_renewal = (
                LicenseLedger.objects.filter(
                    plan=plan, is_renewal=True, event_time__lte=ledger_entry.event_time
                )
                .order_by("-id")
                .first()
            )
            assert last_ledger_entry_renewal is not None
            last_renewal = last_ledger_entry_renewal.event_time
            billing_period_end = start_of_next_billing_cycle(plan, ledger_entry.event_time)
            plan_renewal_or_end_date = get_plan_renewal_or_end_date(plan, ledger_entry.event_time)
            proration_fraction = (plan_renewal_or_end_date - ledger_entry.event_time) / (
                billing_period_end - last_renewal
            )
            price_args = {
                "unit_amount": int(plan.price_per_license * proration_fraction + 0.5),
                "quantity": ledger_entry.licenses - licenses_base,
            }
            description = "Additional license ({} - {})".format(
                ledger_entry.event_time.strftime("%b %-d, %Y"),
                plan_renewal_or_end_date.strftime("%b %-d, %Y"),
            )

        if price_args:
            plan.invoiced_through = ledger_entry
            plan.invoicing_status = CustomerPlan.STARTED
            plan.save(update_fields=["invoicing_status", "invoiced_through"])
            stripe.InvoiceItem.create(
                currency="usd",
                customer=plan.customer.stripe_customer_id,
                description=description,
                discountable=False,
                period={
                    "start": datetime_to_timestamp(ledger_entry.event_time),
                    "end": datetime_to_timestamp(
                        get_plan_renewal_or_end_date(plan, ledger_entry.event_time)
                    ),
                },
                idempotency_key=get_idempotency_key(ledger_entry),
                **price_args,
            )
            invoice_item_created = True
        plan.invoiced_through = ledger_entry
        plan.invoicing_status = CustomerPlan.DONE
        plan.save(update_fields=["invoicing_status", "invoiced_through"])
        licenses_base = ledger_entry.licenses

    if invoice_item_created:
        if plan.charge_automatically:
            collection_method = "charge_automatically"
            days_until_due = None
        else:
            collection_method = "send_invoice"
            days_until_due = DEFAULT_INVOICE_DAYS_UNTIL_DUE
        stripe_invoice = stripe.Invoice.create(
            auto_advance=True,
            collection_method=collection_method,
            customer=plan.customer.stripe_customer_id,
            days_until_due=days_until_due,
            statement_descriptor=plan.name,
        )
        stripe.Invoice.finalize_invoice(stripe_invoice)

    plan.next_invoice_date = next_invoice_date(plan)
    plan.save(update_fields=["next_invoice_date"])
예제 #9
0
def make_end_of_cycle_updates_if_needed(
    plan: CustomerPlan, event_time: datetime
) -> Tuple[Optional[CustomerPlan], Optional[LicenseLedger]]:
    last_ledger_entry = LicenseLedger.objects.filter(plan=plan).order_by("-id").first()
    last_ledger_renewal = (
        LicenseLedger.objects.filter(plan=plan, is_renewal=True).order_by("-id").first()
    )
    assert last_ledger_renewal is not None
    last_renewal = last_ledger_renewal.event_time

    if plan.is_free_trial() or plan.status == CustomerPlan.SWITCH_NOW_FROM_STANDARD_TO_PLUS:
        assert plan.next_invoice_date is not None
        next_billing_cycle = plan.next_invoice_date
    else:
        next_billing_cycle = start_of_next_billing_cycle(plan, last_renewal)
    if next_billing_cycle <= event_time and last_ledger_entry is not None:
        licenses_at_next_renewal = last_ledger_entry.licenses_at_next_renewal
        assert licenses_at_next_renewal is not None
        if plan.status == CustomerPlan.ACTIVE:
            return None, LicenseLedger.objects.create(
                plan=plan,
                is_renewal=True,
                event_time=next_billing_cycle,
                licenses=licenses_at_next_renewal,
                licenses_at_next_renewal=licenses_at_next_renewal,
            )
        if plan.is_free_trial():
            plan.invoiced_through = last_ledger_entry
            plan.billing_cycle_anchor = next_billing_cycle.replace(microsecond=0)
            plan.status = CustomerPlan.ACTIVE
            plan.save(update_fields=["invoiced_through", "billing_cycle_anchor", "status"])
            return None, LicenseLedger.objects.create(
                plan=plan,
                is_renewal=True,
                event_time=next_billing_cycle,
                licenses=licenses_at_next_renewal,
                licenses_at_next_renewal=licenses_at_next_renewal,
            )

        if plan.status == CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE:
            if plan.fixed_price is not None:  # nocoverage
                raise NotImplementedError("Can't switch fixed priced monthly plan to annual.")

            plan.status = CustomerPlan.ENDED
            plan.save(update_fields=["status"])

            discount = plan.customer.default_discount or plan.discount
            _, _, _, price_per_license = compute_plan_parameters(
                tier=plan.tier,
                automanage_licenses=plan.automanage_licenses,
                billing_schedule=CustomerPlan.ANNUAL,
                discount=plan.discount,
            )

            new_plan = CustomerPlan.objects.create(
                customer=plan.customer,
                billing_schedule=CustomerPlan.ANNUAL,
                automanage_licenses=plan.automanage_licenses,
                charge_automatically=plan.charge_automatically,
                price_per_license=price_per_license,
                discount=discount,
                billing_cycle_anchor=next_billing_cycle,
                tier=plan.tier,
                status=CustomerPlan.ACTIVE,
                next_invoice_date=next_billing_cycle,
                invoiced_through=None,
                invoicing_status=CustomerPlan.INITIAL_INVOICE_TO_BE_SENT,
            )

            new_plan_ledger_entry = LicenseLedger.objects.create(
                plan=new_plan,
                is_renewal=True,
                event_time=next_billing_cycle,
                licenses=licenses_at_next_renewal,
                licenses_at_next_renewal=licenses_at_next_renewal,
            )

            realm = new_plan.customer.realm
            assert realm is not None

            RealmAuditLog.objects.create(
                realm=realm,
                event_time=event_time,
                event_type=RealmAuditLog.CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN,
                extra_data=orjson.dumps(
                    {
                        "monthly_plan_id": plan.id,
                        "annual_plan_id": new_plan.id,
                    }
                ).decode(),
            )
            return new_plan, new_plan_ledger_entry

        if plan.status == CustomerPlan.SWITCH_NOW_FROM_STANDARD_TO_PLUS:
            standard_plan = plan
            standard_plan.end_date = next_billing_cycle
            standard_plan.status = CustomerPlan.ENDED
            standard_plan.save(update_fields=["status", "end_date"])

            (_, _, _, plus_plan_price_per_license) = compute_plan_parameters(
                CustomerPlan.PLUS,
                standard_plan.automanage_licenses,
                standard_plan.billing_schedule,
                standard_plan.customer.default_discount,
            )
            plus_plan_billing_cycle_anchor = standard_plan.end_date.replace(microsecond=0)

            plus_plan = CustomerPlan.objects.create(
                customer=standard_plan.customer,
                status=CustomerPlan.ACTIVE,
                automanage_licenses=standard_plan.automanage_licenses,
                charge_automatically=standard_plan.charge_automatically,
                price_per_license=plus_plan_price_per_license,
                discount=standard_plan.customer.default_discount,
                billing_schedule=standard_plan.billing_schedule,
                tier=CustomerPlan.PLUS,
                billing_cycle_anchor=plus_plan_billing_cycle_anchor,
                invoicing_status=CustomerPlan.INITIAL_INVOICE_TO_BE_SENT,
                next_invoice_date=plus_plan_billing_cycle_anchor,
            )

            standard_plan_last_ledger = (
                LicenseLedger.objects.filter(plan=standard_plan).order_by("id").last()
            )
            licenses_for_plus_plan = standard_plan_last_ledger.licenses_at_next_renewal
            plus_plan_ledger_entry = LicenseLedger.objects.create(
                plan=plus_plan,
                is_renewal=True,
                event_time=plus_plan_billing_cycle_anchor,
                licenses=licenses_for_plus_plan,
                licenses_at_next_renewal=licenses_for_plus_plan,
            )
            return plus_plan, plus_plan_ledger_entry

        if plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE:
            process_downgrade(plan)
        return None, None
    return None, last_ledger_entry