From cde4486f8c800359686a5bc027e102ad99d474af Mon Sep 17 00:00:00 2001 From: Vishnu KS Date: Mon, 15 Jun 2020 23:39:24 +0530 Subject: [PATCH] billing: Support switching from monthly to annual plan. --- corporate/lib/stripe.py | 82 ++++-- corporate/models.py | 2 + ...c_license_management--Charge.create.1.json | Bin 0 -> 3065 bytes ...license_management--Customer.create.1.json | Bin 0 -> 2000 bytes ...cense_management--Customer.retrieve.1.json | Bin 0 -> 2636 bytes ..._license_management--Invoice.create.1.json | Bin 0 -> 4403 bytes ..._license_management--Invoice.create.2.json | Bin 0 -> 3193 bytes ..._license_management--Invoice.create.3.json | Bin 0 -> 4453 bytes ..._license_management--Invoice.create.4.json | Bin 0 -> 3194 bytes ..._license_management--Invoice.create.5.json | Bin 0 -> 3178 bytes ...anagement--Invoice.finalize_invoice.1.json | Bin 0 -> 4590 bytes ...anagement--Invoice.finalize_invoice.2.json | Bin 0 -> 3406 bytes ...anagement--Invoice.finalize_invoice.3.json | Bin 0 -> 4666 bytes ...anagement--Invoice.finalize_invoice.4.json | Bin 0 -> 3407 bytes ...anagement--Invoice.finalize_invoice.5.json | Bin 0 -> 3391 bytes ...ic_license_management--Invoice.list.1.json | Bin 0 -> 14502 bytes ...ic_license_management--Invoice.list.2.json | Bin 0 -> 18382 bytes ...ic_license_management--Invoice.list.3.json | Bin 0 -> 22246 bytes ...ense_management--InvoiceItem.create.1.json | Bin 0 -> 1003 bytes ...ense_management--InvoiceItem.create.2.json | Bin 0 -> 979 bytes ...ense_management--InvoiceItem.create.3.json | Bin 0 -> 1013 bytes ...ense_management--InvoiceItem.create.4.json | Bin 0 -> 996 bytes ...ense_management--InvoiceItem.create.5.json | Bin 0 -> 1016 bytes ...ense_management--InvoiceItem.create.6.json | Bin 0 -> 1016 bytes ...ense_management--InvoiceItem.create.7.json | Bin 0 -> 996 bytes ...ic_license_management--Token.create.1.json | Bin 0 -> 826 bytes ...l_license_management--Charge.create.1.json | Bin 0 -> 3067 bytes ...license_management--Customer.create.1.json | Bin 0 -> 2000 bytes ...cense_management--Customer.retrieve.1.json | Bin 0 -> 2636 bytes ..._license_management--Invoice.create.1.json | Bin 0 -> 4408 bytes ..._license_management--Invoice.create.2.json | Bin 0 -> 3178 bytes ..._license_management--Invoice.create.3.json | Bin 0 -> 3178 bytes ...anagement--Invoice.finalize_invoice.1.json | Bin 0 -> 4595 bytes ...anagement--Invoice.finalize_invoice.2.json | Bin 0 -> 3391 bytes ...anagement--Invoice.finalize_invoice.3.json | Bin 0 -> 3391 bytes ...al_license_management--Invoice.list.1.json | Bin 0 -> 9185 bytes ...al_license_management--Invoice.list.2.json | Bin 0 -> 13049 bytes ...ense_management--InvoiceItem.create.1.json | Bin 0 -> 1008 bytes ...ense_management--InvoiceItem.create.2.json | Bin 0 -> 981 bytes ...ense_management--InvoiceItem.create.3.json | Bin 0 -> 996 bytes ...ense_management--InvoiceItem.create.4.json | Bin 0 -> 996 bytes ...al_license_management--Token.create.1.json | Bin 0 -> 826 bytes corporate/tests/test_stripe.py | 242 +++++++++++++++++- corporate/views.py | 14 +- templates/corporate/billing.html | 2 + zerver/models.py | 1 + 46 files changed, 325 insertions(+), 18 deletions(-) create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Charge.create.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Customer.create.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Customer.retrieve.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.create.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.create.2.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.create.3.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.create.4.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.create.5.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.finalize_invoice.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.finalize_invoice.2.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.finalize_invoice.3.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.finalize_invoice.4.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.finalize_invoice.5.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.list.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.list.2.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.list.3.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--InvoiceItem.create.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--InvoiceItem.create.2.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--InvoiceItem.create.3.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--InvoiceItem.create.4.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--InvoiceItem.create.5.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--InvoiceItem.create.6.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--InvoiceItem.create.7.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Token.create.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Charge.create.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Customer.create.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Customer.retrieve.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Invoice.create.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Invoice.create.2.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Invoice.create.3.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Invoice.finalize_invoice.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Invoice.finalize_invoice.2.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Invoice.finalize_invoice.3.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Invoice.list.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Invoice.list.2.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--InvoiceItem.create.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--InvoiceItem.create.2.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--InvoiceItem.create.3.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--InvoiceItem.create.4.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Token.create.1.json diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index c94051a8c3..e59eef8a09 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -125,14 +125,21 @@ def next_invoice_date(plan: CustomerPlan) -> Optional[datetime]: def renewal_amount(plan: CustomerPlan, event_time: datetime) -> int: # nocoverage: TODO if plan.fixed_price is not None: return plan.fixed_price - last_ledger_entry = make_end_of_cycle_updates_if_needed(plan, event_time) + new_plan, last_ledger_entry = make_end_of_cycle_updates_if_needed(plan, event_time) if last_ledger_entry is None: return 0 if last_ledger_entry.licenses_at_next_renewal is None: return 0 + if new_plan is not None: + plan = new_plan assert(plan.price_per_license is not None) # for mypy return plan.price_per_license * last_ledger_entry.licenses_at_next_renewal +def get_idempotency_key(ledger_entry: LicenseLedger) -> Optional[str]: + if settings.TEST_SUITE: + return None + return f'ledger_entry:{ledger_entry.id}' # nocoverage + class BillingError(Exception): # error messages CONTACT_SUPPORT = _("Something went wrong. Please contact {email}.").format( @@ -237,14 +244,14 @@ def do_replace_payment_source(user: UserProfile, stripe_token: str, # event_time should roughly be timezone_now(). Not designed to handle # event_times in the past or future def make_end_of_cycle_updates_if_needed(plan: CustomerPlan, - event_time: datetime) -> Optional[LicenseLedger]: + 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 LicenseLedger.objects.create( + 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) @@ -254,14 +261,52 @@ def make_end_of_cycle_updates_if_needed(plan: CustomerPlan, 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( + 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=ujson.dumps({ + "monthly_plan_id": plan.id, + "annual_plan_id": new_plan.id, + }) + ) + return new_plan, new_plan_ledger_entry + if plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE: process_downgrade(plan) - return None - return last_ledger_entry + return None, None + return None, last_ledger_entry # Returns Customer instead of stripe_customer so that we don't make a Stripe # API call if there's nothing to update @@ -410,11 +455,14 @@ def process_initial_upgrade(user: UserProfile, licenses: int, automanage_license def update_license_ledger_for_automanaged_plan(realm: Realm, plan: CustomerPlan, event_time: datetime) -> None: - last_ledger_entry = make_end_of_cycle_updates_if_needed(plan, event_time) + new_plan, last_ledger_entry = make_end_of_cycle_updates_if_needed(plan, event_time) if last_ledger_entry is None: return + if new_plan is not None: + plan = new_plan licenses_at_next_renewal = get_latest_seat_count(realm) licenses = max(licenses_at_next_renewal, last_ledger_entry.licenses) + LicenseLedger.objects.create( plan=plan, event_time=event_time, licenses=licenses, licenses_at_next_renewal=licenses_at_next_renewal) @@ -431,10 +479,17 @@ 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 + + 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=plan.invoiced_through.id, + 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: @@ -445,7 +500,7 @@ def invoice_plan(plan: CustomerPlan, event_time: datetime) -> None: price_args = {'unit_amount': plan.price_per_license, 'quantity': ledger_entry.licenses} description = "Zulip Standard - renewal" - elif ledger_entry.licenses != licenses_base: + elif licenses_base is not None and 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) \ @@ -461,9 +516,6 @@ def invoice_plan(plan: CustomerPlan, event_time: datetime) -> None: plan.invoiced_through = ledger_entry plan.invoicing_status = CustomerPlan.STARTED plan.save(update_fields=['invoicing_status', 'invoiced_through']) - idempotency_key: Optional[str] = f'ledger_entry:{ledger_entry.id}' - if settings.TEST_SUITE: - idempotency_key = None stripe.InvoiceItem.create( currency='usd', customer=plan.customer.stripe_customer_id, @@ -472,7 +524,7 @@ def invoice_plan(plan: CustomerPlan, event_time: datetime) -> None: 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, + idempotency_key=get_idempotency_key(ledger_entry), **price_args) invoice_item_created = True plan.invoiced_through = ledger_entry diff --git a/corporate/models.py b/corporate/models.py index 715f10d7e3..f2b06ebfb2 100644 --- a/corporate/models.py +++ b/corporate/models.py @@ -44,6 +44,7 @@ class CustomerPlan(models.Model): 'LicenseLedger', null=True, on_delete=CASCADE, related_name='+') DONE = 1 STARTED = 2 + INITIAL_INVOICE_TO_BE_SENT = 3 invoicing_status: int = models.SmallIntegerField(default=DONE) STANDARD = 1 @@ -54,6 +55,7 @@ class CustomerPlan(models.Model): ACTIVE = 1 DOWNGRADE_AT_END_OF_CYCLE = 2 FREE_TRIAL = 3 + SWITCH_TO_ANNUAL_AT_END_OF_CYCLE = 4 # "Live" plans should have a value < LIVE_STATUS_THRESHOLD. # There should be at most one live plan per customer. LIVE_STATUS_THRESHOLD = 10 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Charge.create.1.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Charge.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..1d47b5d3578d995419c585667ee58b070562bf4e GIT binary patch literal 3065 zcma)8OHU*>5WerPXjwTTW*AVk+Eb9oVWrJxwS*j4MV7~1O*^=4%a4Hp?SD`C)sJC- z^r_`4*Ym5d{QRM52pe=zo_uO9KCag%(+RcE1=T`}qH=DG6r6dX6`rXeWmcb#X$SCz zh>pI-%UdRy;y}G+$}wJS6aT2_*FV4iarx!*?eACX`FBQ=*R7CJs5{z%XF@vk`i!y1 zeA?E6n-1`Z3%^GN@{MuP37!ljT^>`nNp_=zhqk%)%!8XW&>BMnla%W=mV=zfZ@PjqU(1wY{uwjEIAOq47Pg3DLo#vMvKX7S#SAgMRx=~X z1mR5bAklhDlam3I#|k-ah4I?PSZ;5=eE!y4|8@EG>hk+lvWC};4>on$+W@J)+(NX2 zSI)-jXF;$Q6i-D9t}PaV^Ll{ffd{wjUFlejBg2Q>m^;f_XuNK2gA^vFr&?y)ljfI? zr|agi`8C_ddtRs)uxo`gVXR{ctsKRmJ0@LDq{EtGa6j06v-)qvldc$Ki=du_!N zf(^sYl`t$7dqktjMyOC8ZQ-r43RQyD>~(GsV(6U~EicVGoeRg&1UkuO#H_Z+PRaI8 zI9B4vW8MS5pEC`f75brxIB3#{Ng9Tf6x*4nKRk-=!n3h zp@UYw#~9A5`*gYo7QwSwpHJr<5-M0@vC1*#0|L(QPTt~TK;5W>Jkbh2c@zqnN6Ogp zSee?VbWJknFXE+(&BbQAQh|tqWqo~b0Vr}AaGc;nYY*rKDi%^fN;ybHR%#)nXNm~tAgie`Y?t#@CBdd~=)E`Y(`toaI&~fy7NU_=(Q+%s zxj)3=+3F4YSXqwY9aMdym-JUSX11FrLdk~{9!X)BAOGaeW5N#6kSaOeLhMYmBZaH2 zBwptgmLCV{1%SFG_th?D5;@Afol$y2xM{F0>;yc_T1i|ljLBfCU?ENin;3Mt)9LX= zJ`U4-2#&1tQSa@gzEnFHyi&>ely7F)%m^SY;NzkvVNkGm7K~0&cHHI2L4Qvibadf&U488YP7(Q)4u_kFxJFP%>7_cv_=ngFgL6PWLt1Nkv93(CJ-$&ANqGSz* z0R!fXTZ*I}pC4ZyPpYbxO8LPhv6NOi1@-G{cFJ+B1H_2`yTMux9j2%NGJ(dF`D}VP zQ4Jx0Q=76_Ln8;9MD#=8YicEf79T%-`EdFE-R;|}sRO^~H8;{=jr%!(OW3NBHbM?Z z6-=LucNni925b82#Vx(ZQcWv63+WA)zEZv$OAHNDt0C^=%4m#R zd80t|0UEQWOh!LDIF68#ao>r$M`RK?#)ipvqA_4~$tWI8wDfY*;h3U>)OtcOv5q;7 z_X$FdVaF#X!I~9xUeg!bF@xn&%OrPq<2SOM7zZ1HV#hHU6Ez*_tG9if?5O8-f9+tM z#58&Y`WpQG=8VEGe}JN|lZQl{9PeB2_Sz6>fU?lSdL=Co4|m@e4 zd8#{Rw93pfyK+>jP$rS$>d#V{#wf-?2{`XIg`^%h>&6+>Zgrhd!tuFWR36N!-;L7{ zs?6{-bwPX)ciy`jm*XL~z!Togkd97lq3{P@B2)cl;D=epzhaR|#G9c%lm ziL$Kb4atmL?;WsaS$4-3BI8#pb^KzzK|L8Itc>a6_u*jOi_W{$;?a3Q8O1jsNuP_^ zd3n;HP$2ZduqTWgxbu%m;RLO22B$|^)o4}Pq;kHz>VNd0JmiGGzF1r=%0A`B@nYtJ zyhin%aofrj5k=M+1t(a%;oSEb2di$iGB?191=07HE=d y0c+@b_9;ik#J)pu!-kxFdh(e60DZ!;2imyC7dumP62`;|0$CiZBqy9ao%{mI5HMr_ literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Customer.retrieve.1.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Customer.retrieve.1.json new file mode 100644 index 0000000000000000000000000000000000000000..17d00481138f1cab3ee72643c2c74fd56b920ddd GIT binary patch literal 2636 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 literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.create.3.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.create.3.json new file mode 100644 index 0000000000000000000000000000000000000000..c6b5486c58c9156185d0dbe4d2833286880cdb0c GIT binary patch literal 4453 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 literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.create.5.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.create.5.json new file mode 100644 index 0000000000000000000000000000000000000000..c0bfcaec063627b36beddd71f00be460fef339ce GIT binary patch literal 3178 zcma)8+in{-5PkPo2z+kp$hKgoPfd|FEs(@$(x8EZf}nOOixrn#b4l5b;eYQLlFQxI zrHCGEX@^2w^TaJP~RoNVB)5?Vi|Eb}4mr0A@HIctOoLMLsqgS2E{S*>LiLvEyBTMFfW zuCH*fkMYi_Q1KIku7Y#YRI%crtFyb%Awa%TQrF$YYg9-VorR8nxc>P5^}9cAetm-* z=VN2(P>R~g?w~!VMrgWaEgCKT&)75gE5f>$#ZPwoHR8O=S!h828(8;jL_Xd;rp&bS8BqAQNagIU+j=#&#u&tGpD@=LdW0{a{u+PH zW=R?~BF;8+!(5XbHiz(or-)g z6cmL6HefM7e>Fd!zdXBG3&U&irsyWxkU-|E! zukWg>-*o#<-E6L>kvmDHm~vk6;+1qA&k);-+&f{qm30lj3&MEi59a3|Tk0hPUvjU| zv`MJcI$lQh3N&y`&1+ey1~Z7e=o~u?GiSkzi34{>auntH$PMel@(^`qh!WMN00*)i zsirL4D{{`6%XX&>N#B8jGezh3K-Q;jD@y?6w0S) zmRD+Gz4RB2l#Ma60I)_P<*w4HqT!M@dOt){k8VYO#dL;A<%EtH8bU&q{(Ahu` zWwjHl1OOsu2zi`TMPoq+64|mNdz~mrxDEDrF#SsrD{i{{afq6*C&c2f#}9=9nIkhs zvlk(Xu!eojFmvN3(g>ni)lP2@8AiAt8d;Xu3fm&D#Zvuyxm<&xJJgtfjZUYu2ArNe GpZo{)%#KL_ literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.finalize_invoice.1.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.finalize_invoice.1.json new file mode 100644 index 0000000000000000000000000000000000000000..453181d7266290bf9df36e29e0f65b50e611e69b GIT binary patch literal 4590 zcmds5S#KLR5PtWsSnw$Yn#elrIC<)kB27>`c2cKCf`FiQDU0zgx#FS7HvHc^JonJb zbz2lLss}^jkaN#BGrrx+GDPxR7_PDVE3eVZ44r>K-?vGeR20s+DNBD|xfg}8r~B=r zny{jOCTT?&XZ*4u)OE>nqM6`$LA$KArd1uZX&(4fYI$pEs26Fpz@%6co?8|RQYufF zv$FK!P`)Jcl41f^E0C9yvb^@Tg-cjcq6CNcs3P2u(o_9AI88R^qAY3dRIaFA3aiRJ z>~r9&oxbD!_;Fh2MoP+a=VzmeAkHXI5HA=l3)PY82vj;@M&Xlp?~i}{?ailOUIEi_ z`!SW^i$YR0_#WFBl$OhqR3+6v+?bNp&vH@qc=SV^2L*7w6uc9uL{Upt*WTZax8gHR zHkfW~NUd;gc?36Di$Q67fjOf;e{qxo{04V>ZY$`5m{Q}KTvrsbHN_x_j5m}p(j~+e z#fp-^-FherqbirIwqb?C<^@x(96D!);-Fi)M|lB;<2flq68d6TFiuK#Lkmo-**#Mh zLTDG!#AYcJvd*XreH1SUUI8nmP~UZZep1a+&s`2ZPVscP#uCLml; z7Z+6c5V%T3jQ5$u+abN8R&}ueThQ`9(`sZTxDS;^M462*{s$bS<2a(ic;79@%JS3oH%!#WW=fh}@PC3Dc#I1$_hX$&vM)5+2J zV0<(?XdVDjX(C+~PbUYn=hNBjAW3TruAu~gNzCb>PAws!;kYA?0F15&i~?a1l~ul^ zuIC`o7#q&C%Zy~^E_iJ&48Uw6`q(+$@c8xmY;yc+a&ff2tbaOrUGekj3Eix^@^(i8 zlrDw1GBv)U7Sy{A0P2oN0fn>ts)x5p#U_s4g`JSpQWVBV*?z^pp1)hkvyZDII^F-) zPGS%usV#;sJSQodQK@i9u`SKDBwQ_otnkVZu9+URPhZzImI$C7YX(8*DfLu&Cm&n5@_&>q9)6>J_ z_nVKb6+Upn?nqeTW5BsLH`BMjpS`_U`N`GSBq;Z&aIROjpxzFoa~C5LWLd%jcmc@% z5Ud+NYOGu3hcD*O&!dHRuB5(rm0=aJy+`&Yflg_w*!PhZnv{M?g%&XhEW)TfTD!^D zPR|NP;GHws$WNA_h4b`ax^F`c;&jt&9>njWtyYh?spfu13;Wqqz>qLLH$WA@(G?sD zi*j0K<6gtKhV`b++>dNH%xDRmtOM+D1mhguhAvNqwIVG{FfkR-iaG2as%gl6U#TU1 z&^@Rz3d<5s_QsLHgXhpH1ggQzYd5wycni184$R$_i8i(md+#_K{26wm2(z8v(tW2y z_pVvsor=gSSgQ<=yE{zsej|%|TNlEAEQZ!JAi*|P#2wDQNNKZUrm(YyOH)wMI~K-^ wb|~$%0U^tK5$?Tli9R%#AU}}7KpZCjXa@R5=Su)*1x~gI4x#Rww0F1nCq~8jhX4Qo literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.finalize_invoice.2.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.finalize_invoice.2.json new file mode 100644 index 0000000000000000000000000000000000000000..9180702c2ebb316ad6394017af48a9b3b50500ce GIT binary patch literal 3406 zcmcImS#R4$5PtWsAo$b*NhDG>;^e7O;-p9Fv_~oqN8ShodaZX;mpFE%CqlE`OQj7oYMc+bl^N26X9!2=vCks2x&^qMS~* z$t~qI&<8Zl8pY6m3O&ecBRmj zZ>X<8ul4>G)AZ+^uAJ6PRzBa()X|(X;2>FYCTcUV>KROXW6qHC4D~$6KC4tS(lIW16&)b$O36W`I*wWKwhj62vz4A% zdP~?XmK2Rk)DhEw5rfkxzY_wAE}$ov@^(cMXHC?P}1OOcF*p?fYSe zgQhfS#|$N7%v74USXLnRFSzkzF`zj{qHmoXlQjfR7F5I@49&mfk_vv$YCvg$#}^H+7vvHVnIfJsammi_Q6m09^DE2l-7JD`q@2PfPR=|{(a8`->^p2`^>|D(3rS~N6cz;OD(qQk~KrrA0_~}2Py>|$*c7UkURhTI))Su zO44doyHI8Pm;8M3;btLUoXzHcuP<(fo@<7w7~*$l#d4+%i7mF{xz$vfrP2+#aa3Av z_r_=MTC8Ob*iS5nq77Wd(vfbCPs2&k+27)lnpM1k1r4e|EZ^T`S^>fbG#?M-mXF18 z??Z0fV&4)7KIFJGf_wei*kTR$Nn*r^<5Z7l41&6WWfB2G>~z@W*XOpFeV(TfAAP8P zJ3>>eC>BJI#v||hL2#3XA-Bp>5l%eFTM?qp;Szgrk5<3%3C_ zNXtW3KlXEL&*rb!Z(m$R9UkXM1h%0|0Ru^gYa=c!>+TEzn1S^{By4okcX5cU)$F_E zU;r7MAf(GgnzMkc`wk3n4HgC7L1~*QebO`UEclyB*l*(Nk8rk1{uxs%1KN7UwxlFA7mdcvnOqqq$LBobV3Ar>2DMc zspDPzwCRfS0vH{dW+mm4%j@0^r3BBZ<&`9J)>UFHD^;|s5f`Wo70jtHEGTxiBD7di znWGG=U7*jn5J6{_FQ~p|gu=-Z^yO5D`|!$mh-gMlCEGgRf1k1h2 zC9{hPP5FxW9OPPW@5p;9c`J8XGnspjJ5xk;&VYer&Y37oN2_Nr?Ugx0&dx7RkKVk# zdU*^2NA1f@gDVQn%*Oi!10hh#f|i2WpC6py>RDcuJsJI==g|P77M1KoDsjw$D;50R zc&ol<>58xwh7^@cl;LBSZsP&&AN}bM&ndufaJJ{Rg3YNDmMD5(GRWH$gQT)PVJfG6 zKCE$2l?LhPp;{R;x#kMP3dH6HH@+}BG{;D^EuCYsfQOSA6)^`x_RqPbf|ghhM$T7U=ovt4Sx8Q%?9HqfN`Vo;OU_GHu=%p zR||?sLT3UKHsMlaNB8n=?%iw4s-4UM{m{$&+pY<|WMx%sQV#_}qvIaGpjV?=)$B9C z80tVT6R)tvR!nn`ZHFo_?(gr7_omGSU>ZxcPxHz6U^E>a>`jxlwqgY} z0z&GfkLD;1P6u*F9R-|SZg> zZXK1D+l}wD_X=Z~0rnHiA!r>_F?1x=@d4EFHa00(&P(XfAPdiBo6}5Jfbjv(`z6`3 zvX#P_akEWbAZ93bjbLBXH(IQ2L#Y|@$+4{aMFwxag>DiNLae2jpkjG)m@n%WrZQ@( zBug$E{q(o7?+S;2!KJ0Lpt=C4gUN*5#Th6*rTAwkH*G~5dQE6>e~ogTYujsUTkT{` zb^MQV{b@4q=c9M0>ZTPw1hTFD2ASUj%DLZpJ~Q&=yl44;6XnJ&F#S$o zu9aig4|I2OcQs8g=K8^|WZFn59#9&k5i`#R!elC7G(goaHOjOb1M(v`EY$Hg7)DZq9du^Y6C2uZd#awg%CExR z#uZ+QXC1catWr$2X~fSde#%FFXrkFymo)HBN%ajZWlq+cKaBZdpR4Ug7s8JoYeZ;r zMZNps*_2RQ1`VuCpTJ;T>vt6xJb+Lu%Ww{Xh{1}2k#ESD`Mrz27&grR literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.finalize_invoice.4.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.finalize_invoice.4.json new file mode 100644 index 0000000000000000000000000000000000000000..432094cc1edd1003b71d3eca0dae4c91c4ddcee4 GIT binary patch literal 3407 zcmcImS#R7n5PtWs5ct#rN%r(Q%~NC8Ns8J|n$%94pdh#s*UOqBX&xIN@PF?NNlB|c zg1poZ3u|UL_k1&t2Sq_xS?ZuXjlcGuyei1$C-Q8arC7~@u7Z?_UdIe z8zw5C_h^>aOemqQ`wxtfqGVoZMHjqp!h2qu>{X{NQ90^gFzI-*8@#5hS}|3k4sS!E z&xMr9Bq^87UUSMovIcu4lkz^rn1!h2%xMMBNzGJXGI7&8ptiA>T1s9Q$=cxTP>A<-5yP^USk5 z%I~nGXk4L=oCb^-oJIi=gZ}zYZflwS!)vLanqYISn>d%6sEp4F(YM3+KU0j`bR3sR7Kd@Am_A1ajRLxoN=Ra4H`D z{CJg05T}(0wW*|$o@TM^m8ljG3t9eeUJcMfYyp_wSfob)BS0$1Is?;R<5y86YUHp8 zNWg2s2+8nd*OX;e)hju9T~z|h&tw6Z#FgX4Pv^`Q<7dTqI2sqvi#L3pza~FN{~ATP zN!VOs!?w7T**U#pUwUWjx|-4pKtIf~__1eFtaz=fUFKmxSWevI6J|BLrRVGMpVAO{a&gwROV=W(1Tp zO`qK{7LpF+o;e0MyV?;H7;9kLl}jF%69JEDP{OaJ6Xf>cZ<~kx9RjaY?V@jV^mg^f z`08Z**UQzlIlg$yhR1(TWH{;D+g}M#C$+u_hTd>Q{eA*~yQfmXQKG!r0OZ#Hu8t{% zy^8Q_;uo)`}P0I+|N-%X4p;atm#1dK;MX z!tadF-y5tY0qmzgG_FVJ ziY3E}*p2xhT|-FWnrR?jdnVJ32YDN%+>*Ru2CKL*aOpVM9Q%-JI!J&FUc`Y0Bv5A%HWmKd6G0?z%Qkkr~T>NDd~D z!wo{)Of-X*pmo=U0j|NOz(c56XFA@9U+}CeK6$1pX7h20vH^RSXv0;CKGA3#;JE&f zpD>tw_j*CRV)h2Em7toF81`Q}bQ-@w2urj@g!86qjv|z88*0x4;$+4K2ItxhPhcS7 zyRn6z6bc5b8w=l&vGM!w;wbDIXOe&n7D?Wit(I?mz69`gkY;RpDE8(_ckp!ZAGTc0 AHvj+t literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.finalize_invoice.5.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.finalize_invoice.5.json new file mode 100644 index 0000000000000000000000000000000000000000..a5d35eb489234fb0fa0dbc5c57ee73f5196a6eee GIT binary patch literal 3391 zcmcImOK%%D5Wf3Y2zzd#$hKf7r@D2U7RUo}5+ra?5Y#SZvEq_6$(8LG{_p*Up?EbZK7TBJ*=A{83($3=RHE1MMb!}X`1A!n z+jPri1;&8pM9rm^`eyXPS|v*!q|t0ChDJgVwatF@>Jn9ufhAX7B*RTGj8_}3OVSZs zljsYnRI*u=EADOtagf3&o8((E}jfaD5DTS*1z-nX&7mYQZy?cN4_TtUo*MD3> z<81q}a7abvgx~v~Q6Us*S@Bwl@ay+RN&CDs^+?CK=w)_**_F|QN_8B+lGdht_t|Qn z1%A)OJ(ZMIZv z;gBf@?UbR`nDC{OmdXlZ|3dmW7z0`(SoW=x6S_j+Y{6CT!PNarskxHhMa4LUJql%M zOo$cj)U1q0uP~`MMwWs#E>$W7>t9W$2>H1W@KR8{rdeu=2@b=4Kt~YQ?~rr<;oto7 z;`gObC5Yo%NIh24O3$-c_R3aE#8RyOH?O8-A+`XfHx}s;a0IME);Uani@zq5M2#FC zAql)5j);yo`^GG@szJ?*Ynp_~$G~;PT{U?&!FY3w#&oeUcl>AyXYI6z21B{{(5=*^?Y+DTW$XA=+ZasU&VRAVuF1O8RyjieLg) z>7qm8W+}Nv-X_ycTn@BgJhsFksG3g`_`-`g(SVL!+}Z8pLW}!;IrDU`krVAQNsCc8 z^&%ZJ`H*=Ae%>>EOgWRWd5U~icJ&1zT>w%C90JrKR;H|8_mlhO=F_n=>#M9IA|1=n zHgqYWAnTBA#KmUrZVmuVg$~wO-@fnT6q$A6r{pk#0`~^%GSN<2!q$D)1YE%CE130w=h=dZ5oRcENNVbpFp_y;iW==#oZd0Nu%5I7Yi^QHLc$eq7v;8 zamrNf0YtK0Lmikvn#|b1aIW9x1tt=N))@U{s92dQ-;=5HM-SsL>>KDJVHtKwUYebn WJF!>+zdaI7ZI5z~54fYJqyGRI)ylj8 literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.list.1.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.list.1.json new file mode 100644 index 0000000000000000000000000000000000000000..0d1b946bd9c889a47c3003ccb2ede4e2cb5834d2 GIT binary patch literal 14502 zcmeHNSyS9N5Pr|EnDSG$DhbTNm8T7a+z^fsSW+pK#?~+bTQ)w1fmHtcwhl}32?U10rr`UPk`2v4;I^V=^_=;UO%vg%V@Aw)fI7f%hlkzab0f8}= z`Mwzo#7ZyYBfVw*Qyz;j^ zBkU){5R;`L!rlZkm+zOxnHd{W-7 z`(}n*4P_FpGs=)sFI`V>pR((mtFdZcHPt7BEm;NfTj=mG2or zisr(6!aQIQ8e(7Rx3RVnW!R@T#6y^~r{=1oFieG36t*0P3Fsf9Y!XtJpe&Y>PSYrP zzO)2vd6uL=J>l|RQnQx8lcYtx0?p#1N#(QF>gE0}-I`ebMz{-yvhqgxioTI`U z;BL-%X4Eb56SUfZw-Cb2J!sjpa_?&VtNhQw!c_v91KXC>V3S3wVXvSn&I`|@Ty3!5 z0Y6DtLOPFMkzuFz1blF}*Lm78|MmaUbmyBX<-$XsD}TXHswqkYv`GYm@}9XMD0pTr z33~UXKAn&t^qNQk!^0|UefyqEK#>^^&%v$o^SDHiIJ`6^A~6;I*2qdN2AhGE-TunP z+S;HuSSc<9ktQjQg~{}L>)n;^`d~#xM7cqkmwp7s z3#AkUT_K-~9BKct4*sKU3i60c1IS8XPd?XaSKBy**d$|6ohz`YFPVswWW%NiCK=`M z$S3J8#y)Gf8W{6I#TMmS>WmxsatRSpc`Lu7vOe}FuQ%L@&4Gg17A?}a=x|`%d!-;Sr|#I&*|QtG z+`l?qDY#=DoHQx}6&+(9T;rP5sNiK1I}viyFvY$sVnFEYnGoqH@Uk=!ScK!51VBks za@${$`Msh{2aHC`s`rWxBU6JPv8+r0VKoShWx-J5a)rsCLin*@j`6I@vumq&RCT2g zx#6P(t(2vJU3nf5Q5hUR84vPyZMx?leE4MTC30n?5@Q|##ZS&io_#_tjdWg|Ptc0! zL;xB!WQLR!U6cvgRNHIVg zF7WvoPgk5UBCPx!iM&prFFwa45YDYY8}vHB@dbeDjO7&}fM61r+xLNqT&%SN5ZJvO z%kwoAC2dK{xQ7r%0)&@pb&#mRN1F(dyMe?3UUCg84slDX*+dv%Q_knej2IA%f^&r} zugGu=aVY^I@c;&@Q?6Htn<`_>+5TXq%WfAKq7_}Hte~7Cv#fl#E0?8(i^63a5N!mY zFt8|GE((|6QjD?P>Q9~DUAXLR-r(c+CPFA2+L&1Mtf>m2&9HDOse8!6Wx>}e_}X4l za`KQX>kGAo%XgU77A||UC|sJPO3{CJs9agv4z%i%LUpQ1=YM^)OJk4E*V1nPWYcEB_gMkuuHnuP0_EO(0A(wz8ozp9 zplp>b((TQyi&q<`+S+m;ya39708lO@JscEQz%aaV+=7!!d;E7{vdPYW03ut99s(X0 zT6cJ(j@8yvEQ2ljdKP^>+9(zCnqE{QS&xzR19gl~5@Sc1(?L6M%EjzNQwnJG5+5=}&jWozr-R187 zxU#g}nrH5hD{sN_Mqre>8mXt~N(P@(DeU|o+bAvd-rHN&RoZI4u=xIa*lwA~puk-2Q7w3K#w>8@@J*N$us z%&Z{!^PM~0*%l-V4GX#x#j$#JOqOi*DK(WM3x$jvkLs44!eOfOW>Iid1xL}DksA?@ zF!24CcjiRuY0N8ca)3vYYH AX#fBK literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.list.2.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.list.2.json new file mode 100644 index 0000000000000000000000000000000000000000..52e883d852ce029c0d9d319d2252cf388ecf11b2 GIT binary patch literal 18382 zcmeHONmJWM6u#$IsC3FyO@eKiZB7#iS%Clv%Va90YD?|7gWM8Yj6*8_eO~Xa%`kuy zCzt~T>h1MgzPEJuw?~fShA2nwU(Ubqx8r={zu_wig1Df$&wr<1p@Vz;-u<)MOwkxa z8!e*9w2k@5uwX6T+J@JuHYJFJ&{q4N;xQtW(9iXONfMC&3qsuM_p{A3i&4&E>f;njje3y6joxKv=_%&kdlO{1vHdL zl8l>T?R$ZJNpq<`rXeuMAD~F;w=uRMp(r9(IP?)?Ps3J+ah!9lC`~zvGtfVu&`C@J z>=&t!be<>KUn?uXmZw<`)MF;^71?VAa3nA56=)V~&1#>OQg=Fia`oZp#E6Weg-d94 zA~m9Kb(Pd5;Sl!bj^;+)3R^*|4WI=VrozuEt}<*j=qmnm5eG^jvjmc|vCX1ob68Rp z_gNScrZy;Y04Ficu=98irB3fpr`PHBoF~o;Jdpo+e`vbZWKy}%AY#g2!b#ahih#6< zV68f*QWFBm%p^f>J~Sc|JdVRQQo!(#bXwoOD+x$4u8 zMgjt#FlpI15TM*m7K|`=ye{m3{#+T6En2>9NhfcAjTB{3=UI952YC|@n8f_DFYEP zBLl^$hR1xL(i9cb?=4U6aZOf$AwxsZO^#$nVFh$Tx@i*S>d0lLJS*~Pjd-A8nJF(b zWxy0;ZP((d`?E7;Z=Y;Wth$^Gd|Cqw&o~p6pv^H;7S!Emrrc*RsZdgeg|l-QSGimf z&^BhuCO7^8nX(D0B>hSd*JsMLp>5%yoOwnLRSII~nJM=+x~tudhnOk9H-YZ+HCw>~JqRuZaGaMdRWv2?iP688+-_ ziN?ahK)6=jvR92EXOt|Yg|<}{tmb>77Nc-! zr8x#!?^?M0U=ZZ>`t%?=+Wnvrgq_2{snW7=`49@1m5ST#Dwowse%;b#w+nkwi1B|a zmz9Cb!sW7XxxC_2KkI;tbidyfpWfCLI(=*W=XHh4p4D~!S9}C@_gT0sVV#T_?_Rik zgJ|OwpWZyK_?Vzd(tq<*xi<7?r^+f1R&Gn|2$BRiVwG2XdaM0)??GPiIWU1P>78z$ z#M6CkeVS3TCCGB=MJ!wn=;n^M_0Re7MSVw$o+{ULwY_d>XNh+zTXy^9RTuPzkEOAv zU)J-kceZ7-U;%IV^cEvgw%qFdCCXOWBH!8CK7XG;Zq8FZh6C} zGRLo*Cf_PWwg%ltdR%(s!k)BP4`9mo_m(eUmM>t~IUf9|2mo$UEMLI`%I7faM-T&Or#jC7xiRw-aei-Lb!NJa)0I5;(f3FH0WYe}+5i9m literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.list.3.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Invoice.list.3.json new file mode 100644 index 0000000000000000000000000000000000000000..9f949ec1c928d7c539bd740a14ec538cb8c2dda0 GIT binary patch literal 22246 zcmeHPX;b4k6aGHGBIQr}R_!b#P6+I$mupU7fZ5KSE0_y~ci`S07ZWF5Y^ zl9{l0J}_WitycHb{m8PuziG7$2gS(vv-LOp-fDeMf5KB_+kWE3R{Ar%g$l;$SL645 zv4a>%OG%xjbh?^)({s5jbl6s zxV(DTtOU~mhscev6efv%3prEd*|fhnOk~-BxGs~!utzAo!WM$s6Byq{u6rvr!7ExJ zMv?D9H^u~c336o()*ebUE!n>7VmqDA1ji$vig(XYHjELc^Jr70rvJob>?91aXQwNb zM2<3i5`o~>kYLw|3R0wnI7|mmBJ1GG@&4xC$IIOv=)Ilq1c#t5C&W=@KNej;m-aS7 z6Boz-ze`*a{9*f(lD^7}$L0cA!N~Utt1L>R5eWjeKDt)3<}sRCct%Zv3ggkf6jf1; zz+`A8k}Boze?%$x10A0-A*w0hFCXEgsfKNCt+$=rKNP}5JsY!R0qeA z9g={W1r+ElNt8Nc;dz2BF0)o2dk!eb8X%Y3xAe9l@sLaIuwx;ro|35!eLqgELhEwm zM__*z@uohpv6X~urQt?50%r(wGe!%OZiS8@%m&aRUCazY^TXIp*_)e85;A4nKsP8{AW%E(}!8brqIv!QNl@6Y(65x=e&?=XOHwO2B zmThyRZMIiC-JaeMRxgTCn0klV+vxYZP=yCS)xIXWGVcb(TtoEZaODRHVyVS^%k@lO>9@O~FWOP=F#t>Lehzp5ahrfQPCO zDOkXY+suOWBI;=+oum#}1u_Q1V2GJKfzZ0V!GjURr3e;@x*3>GJ{eAD%AGM&``mc3 zxwbCxl;?q9oGFw^i)Gje+)~owT*HPH*QGv{DGwAhthiyt04dVh?!{B%SF_?SxjQ*N zlM#X^K9z|@SE;&qR?MWECo9&lj;Dq7B*w@>F3V_*O$Rap_#L?|*geEvgj>Juqp;Qa zz13N5cYr0o!vp@8`9Gm9K9=)*s=Su(lP6S+twPMNrpkq;Rp}s?wuVG$N$iuUa>j#Y zqRU4X37ToHuJ!ue*GQGmWTJ~!xBfESWGA=ukY&}cAg!>BRQd4q-TsH&y;bwHIHRSe z%6)TftzfJQn^vhZt*i3HR!i_OZso9b@KCCp+f7^b z#y^26%cP3SpDo1wFO>^T>v>lB>k~ezOz58RQuzT)xwGExwbx%GQ~oLw-JMT27hi01 zAiR$)!i#0fvxBd*jr|XU!|mdXmYOO5vX{y)gDEQ}>mqmhe#4$>OgYO>srhEFia4H# z_x9~In#!s*!OS0r(fS+2g&y&dFN66!9*)L^rVSdG9TsCxUZ6wm#Lk7Bb|cWBkT zx$J3hyr?5S9f{tLJmO<6$Ht{Xb7Y>&kzIKGV~_ZpNCf%VpB=i#dnZa?&Bi6T*ef~W zqwm!{@rX~Zlb5@7*>1xYWQG`z*}2R$Y&I^Njmu`^@|A2{cDC-&`DYnF@7uWSC{-7K z#D_^Y&y7nB>v+!iI8t18#OD+8N{{$-7IDN!CRJSihg0Q3(|V${_>IeKS27D*Y(^}D z;a|gadfmSHDv$Ua%0!oR&bQC}*@5srp{V*5Tvn<)@HTeMt-r>n*ToqvJ5?^&YQ+g3 z*yB`L;^YW_1vjpzgN~TybEGqvfqh!Y98QO30JIN!DVH?;nCUkIyP5t z{6x{@mg@9%tCe}C3Af=9v7O{d-KBanCDsf2*_w6+l>@e#4CR~>%x~?|HZp`6!Shj zT-!gMosnF4r*KHZn3k zDsJV`25UYkOx;9WzLQ?QlH~+^Bl4$TzEixb30!$zSMUfX`NSa literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--InvoiceItem.create.1.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--InvoiceItem.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..1704e35403d257a2ed4c97bffba5fea4df8e67ee GIT binary patch literal 1003 zcmZuwU2hXH5Pa{iIQa=AkS<4rD&Byi(u%ZcMNtK+EXTJkOZ;*1N7NAYzjtk4l1rOB zUBg9DoU(xObICMuOH6)r*Bc3)0I5ad}9as8BOcJ>csN`v(p z7(f@%4jkIle@z)tO5~jW?9WQD55gsZ-2u^yc&9{4VC$)K#bhriWFV77*v?CGGgOKK z;$Hp*oemlK;L%{nu81KWQ0xc299kq*loWG!J~~}qHItL(_IUZweYrTDPCrhrAAjLS z{ZMWsp{$+9j?W=olFdMbze9TrST3e>nen zb$+`jn;@2hG#cfy-#s8W&-*nGdIcwpzqd~FMHD7rc;Py%x*V-Fzyp%g-b{*_zkVQO zU~s})kf?=9yUA~e?H2B=6aBs4E|m>DjPj-PaW;G*8{;g9M^5$NN&3%SrVjcNxO=h# J{D7aQqi3n`8^{0v literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--InvoiceItem.create.2.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--InvoiceItem.create.2.json new file mode 100644 index 0000000000000000000000000000000000000000..7e972e9ed810f6268e13c0244c8087758ed05af8 GIT binary patch literal 979 zcmZuw%Wm5+5WMFr44re3Y`HahYmp);&^Sno6h(r9ps7u|HATvNxCxB>dv_^Hk(>A= z4|iu~XUW6KY*xtDMiYt;vwBsQr;MoR92oUPSP^|wWY7oOf@4rcFV(P7jmPS;9hDEsTUh=K|Gcl z=tp5Rdy9%&P1HJdYTQF>n`62bf+UlY3J=ez%x?aG;^~XbfdSfnnhqRV);~}v6bl&s zQ!VDJYFVx7CHE8TgLF(l2g~~XqOO;O_vD?HJo4zNbG^w-C{-YnR4~pDZJFx}66~jP;ze3~P0T#nTPg7YG;d7>4ZH2&VYR_vlQ0mFZ;!&8@{wFX2$a+=L>%wl{H`8|;Q z1Q!h`Y^CPSP(kssY-gR!mwa4kG0}g=B888e>2nxN7ePJ>>IYB6cfV5UM3undbxzP% IUY<|>0kHWN+yDRo literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--InvoiceItem.create.3.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--InvoiceItem.create.3.json new file mode 100644 index 0000000000000000000000000000000000000000..85dfd73948f6c339c20bcc714303478bc0390397 GIT binary patch literal 1013 zcmaJ=+iuh_5Pk1gM0oS@%TQC6>-m=xj=a0u9-hDqi z&9W@-YZ~q#FcH!}v9kdy40aB+3eCq&gW=3&B2j>{kUV?ORZ<)#MV1%Ii{uT|b_S`glocan!l zCQqH|O%8-yI5J5E)4W7iqm-y_nH9gF4IfkT!Ms8jT~UQFKzoQ@rXU6{Pi>Yu`V^x& z%g?u;ip$gD>vDVBovhBW`11N%->pXWr*b0-srA}-Yz^VB83!W#pF{~$DAtNc^Rvpw zr27~K%bn4U?>7*R?CsUXrdF@c%9C&FtIg=jKp2g3*;jW6#Br;?z`IN15GnTLZvb6*}p literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--InvoiceItem.create.4.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--InvoiceItem.create.4.json new file mode 100644 index 0000000000000000000000000000000000000000..4df7018f01f0dfbd2a375cc1e391ff7ee37f0601 GIT binary patch literal 996 zcmZvbZ%^AW5XRs0DI&k#q$V_@+dC!@n>Ii&O`1T+avYZ(b!_85-B87M-<^|&bgf?{ z=ey^Ae$L(;6$KNWO(wEWMfqWd>m9{p@?IJFf>D-2%Z6Zxwo{&hG6jBheS3NO{p;QN z8J3lOPAj4!DN@QSSGFoh4=#c=RD72-xZ+0?rWL*|-WS+Q{SkVQfFOxTG@7KALI;&q zprvAf-NxHSvsvBOgIQaURBDpexl+Sgb++A=>r@noD==~Mn$v9aq$D}P1`UX+>qVXN zV5$EG?LmW-p`Y@&{&;dSujVz?V>m?Nvz5oyqO8lsyvBGh?zEu8k4(<@kb|I*5g{>Q zJTJ;Mpf#9B9^`M;(I!IxPl@S?!lKJLzZ^MA>ylqoI0s7=$ zeqKMPZy=js&X*&VS9CP=Ww(0-$AcDhd0zB_kLnx?dV ziqCid`}u$I<|Ij&sBB{byGZg8u2Txh#yK$Z1%s^dCF_Dd*a{p4W#jpL@#E(5>*t>z zXIPfDIi(1I=P4=06)Q{d(y2NqYpD3LEERnVooFS&cu3xSf+ERIlWde`$-88?8^7&@ zm6wDm3QY|jg!T}%RY{Uc?c;3Qk4AZ0uguC~FU&QK*0EBn6;!s|m#ZKMyoA8S&1+1v z#b1zQ59>5kfT|U(!6{4qH)tB#28K52cyfMrmS_2d>M`tta3qk9vuQd>r}+fqt$3{k zS<*Aru7&Ibg$xLZ3H^Ohu25Pl^T@sY0~H-I7LR-6RYQ6;eYql4{_<^GsGfFOvi135V zoztxi9dhk#*|Za&Pkw#3cv{bI*XK9a#kUVzDhG5m^ku($P~bf8#ysd1oG|{ux{5zF z!UPrmqYf{1wALEnK_O`eLpJmN#Z1xO!vkJ|RF%*)8p>e1aXafo_YC@xvH?N-WteY@ d;e**?vmn+S>%pV)uiw;|9p@Q_4{2Uc{sD#G9=`wp literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--InvoiceItem.create.6.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--InvoiceItem.create.6.json new file mode 100644 index 0000000000000000000000000000000000000000..be232f9485df00e799aa11f24589a9d828fbd4f7 GIT binary patch literal 1016 zcmZvb$!^;)5Qgu03W28a*UR8?LLMK{DFdmXO3*nOdG|AH}Pu?Y0u2u$3* zv}tzr2PE0UdJPqz?nE1K%2NL=nufN4p`WanzMst|`D9A<81_Lp63B`oEvK`|WQy@l zywQRz8JQZ_LH2?|286_f@w_OvQd%qX$i4gxHElBF;L)hiQelR4fOBYBMk9KbW0S>- z&c(Jn&#pJ0^RM&#+u7!^xwyL)>BaX#$N8Xs%r}(K)~;j2*Fd*sI1t7U9L1PXa&1Hl zzmxgubZ0}4+&EjsZUVIAH_Q8{Ps^*t&E;)cEC(tFbTss3zk5*NJnzT6(F;F=!>sO*-C|~cw0~}_@hiGfl)J&S{aslR3<{B zf)y7d9yZ=L8jWUcJ=(29&80RQ(^MMVLhY(Uxf+rpTt$hymnO}veuHKwM5loX>Q>Z4 zK*#-eWDgkxmVe60ML8))(zI2br%EUhKDa8k6W~wz z>D%H-&p#S}gUiigPZfZVM!p<&PY5B(Zp?#TDJUBsU8v<#RyJY!k2*d3##(1VCM41h zhHaMpZ7( literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Token.create.1.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_automatic_license_management--Token.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..60d47536e7c17c03b5152a712f27efc6362cb64e GIT binary patch literal 826 zcmaJe?5488L!s+`eO3a8zWw!@@-X^0(msZ3&4k2b9m7p)-v`{F>^Rw2?$YCk`} z7kfWS5~aZzb(`$NJ^X<&4!zGcaTlWM6*R3$tMMRRx0YQ*EG;pbWWm7bhq#g~PK0M^ z4Z4I2OgzH)A}rSh>WzR~8?R_C;j)0RZCMT!dFg*SwWCH@r*ejjA$O(k%7K-~QSaoT zFV%m%FV4ZOzQSWV3l-_S1kbboBJsP)tJ-oA!qbx{iFF7rgtBznN7rOO(KU`Ni=^k5 zx2O5z!^izyDreFwS+~{%q(HPoL)<4QI+1bbgW5>&m!sFIX4x$3>nmh1zz~@Chm+D^ zOCDX#4af#K*MsZT7wV`p?$-&<^#(2NfT^~5;45m;B-0G*6*-kI2`%Txk)=2Bi=^Tv zj93aLz2H=M)zMhszV1tvw8g46B6tm@$5{L;G>SX6;;xCd&e~P$W9L{mI*fh*S9axJ literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Charge.create.1.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Charge.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..bea8e4b1fb5f30ed21d95351d8905d70c5767349 GIT binary patch literal 3067 zcma)8OHU*>5WerPXjwTTW*CUH+Eb9oVWrJxwS*j4MV7~1O*^=4%a4Hp?SD`C)sJC- z^r_`4*Ym5d{QRM52pe=zo_uOHAJ^;k$#g_5bV0SyqO6=7BL!z(XoY7gNSW2AW7+|{ zA)=$R@$!~QrZ`Ye=Y zmI|9$&dILP&NE55ZeuyfdHkj;DD$;UDdwLsBa9OUOl4tdP%e zJ9y=6tbG;)YeDf;wBXueAvmuGNFI1_%ifia)i^SI$c?$PtcAww<~B%SVtT4&wmoTn z`FOf+9-H%DvvIuWg?fRzRyY&JLZ;ZtQH;7{(&bD#EGlMKyD>tqo)3r?c2{ee>~xle zb`GsvUJ(rw-Y?S|%d;+t3boUMFAPetgCV0~3T!~=w#+j$`kWo;?Z3d|FrpJ?F{Tua zR&)zJlO5F^%Ekn=MMy3Vg6G(UXfFdwfq&5UK7k>`ks!<%t9OWx?Vc8Ca)Ri?hP%aN z$1~yXse~OwSQ4e}z~rbOWsYkb9cJpl8xEudOQ)jZ)a@e<8w|}z@NilU_`Qi&2@Ag0 zRy-lvFzj3j!&0$FIGSvP3gyuj-Wsb=C0NZ~=LR8&-f7YD(!A5Ta2!pblUzp3YK!!g zZ103)C4M~SJ@ETE)!oRIGT-q7VKJ176)loEm0gdH z2s|1(Xytp1;k>#}r+Z)#Je&3TblxGOf;ASa9AiFU;0*8NEp7(XjY>!qt?-jap^$o{ zj4h9qseMYRtTn3=aFI|8d()B zw_=?8LmZy1-k^__><|s9lH)DJ&O|#> zxY|nMb>3n5d61p}s9SPh?P4a8qukpWrALID2HV0;z{9MS#P!0M45kVe;&iZyL6=LN z9^d5SFwKYH$UYzS-rnj%`*> mKqRW=HSPlE0P%4iVf&U488YP7(Q)4u_kFxJFP%>7_cv_=ngFgL6PWLt1Nkv93(CJ-$&ANqGSz* z0R!fXTZ*I}pC4ZyPpYbxO8LPhv6NOi1@-G{cFJ+B1H_2`yTMux9j2%NGJ(dF`D}VP zQ4Jx0Q=76_Ln8;9MD#=8YicEf79T%-`EdFE-R;|}sRO^~H8;{=jr%!(OW3NBHbM?Z z6-=LucNni925b82#Vx(ZQcWv63+WA)zEZv$OAHNDt0C^=%4m#R zd80t|0UEQWOh!LDIF68#ao>r$M`RK?#)ipvqA_4~$tWI8wDfY*;h3U>)OtcOv5q;7 z_X$FdVaF#X!I~9xUeg!bF@xn&%OrPq<2SOM7zZ1HV#hHU6Ez*_tG9if?5O8-f9+tM z#58&Y`WpQG=8VEGe}JN|lZQl{9PeB2_Sz6>fU?lSdL=Co4|m@e4 zd8#{Rw93pfyK+>jP$rS$>d#V{#wf-?2{`XIg`^%h>&6+>Zgrhd!tuFWR36N!-;L7{ zs?6{-bwPX)ciy`jm*XL~z!Togkd97lq3{P@B2)cl;D=epzhaR|#G9c%lm ziL$Kb4atmL?;WsaS$4-3BI8#pb^KzzK|L8Itc>a6_u*jOi_W{$;?a3Q8O1jsNuP_^ zd3n;HP$2ZduqTWgxbu%m;RLO22B$|^)o4}Pq;kHz>VNd0JmiGGzF1r=%0A`B@nYtJ zyhin%aofrj5k=M+1t(a%;oSEb2di$iGB?191=07HE=d y0c+@b_9;ik#J)pu!-kxFdh(e60DZ!;2imyC7dumP62`;|0$CiZBqy9ao%{mI5HMr_ literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Customer.retrieve.1.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Customer.retrieve.1.json new file mode 100644 index 0000000000000000000000000000000000000000..17d00481138f1cab3ee72643c2c74fd56b920ddd GIT binary patch literal 2636 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 Goc#yDSd8NU literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Invoice.create.3.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Invoice.create.3.json new file mode 100644 index 0000000000000000000000000000000000000000..81e2f6dda8ab97429f555edfb292ed889f426ecb GIT binary patch literal 3178 zcma)8+in{-5PkPo2z+kp$g-QHPfd{`y~Rcn7$_1H1hq?9thwAZFS2aI|DH1>m%FP= z5k1(_42N?&bB0eRvl$ar)du5v{A(ZB&og%Ynf)+U8PQ0{P0%{Yn`lu7lAgVOiO*Qx z2~|Teky+UYWt6$uvFJLjD&bXY_)1QlcrTkS$2DA+q>>!0gmyAH4!-50-Uw6C9o~i{ zUn;GWPgbpky^&ntWCMSd(E1@|nGbO*g=-DWStCpkI%$&~q$T^R)mm0DV(&~(dMG+O#!9)nihk5$|3+_BsADhI&3b!#S_DjK&|U6<-TRI4}h zV$0>0T1w6h-I2>c86#*qpkmTr|Bad&b;H@6vej}Wg7&-<4~+zFOAm_6{)B6TjdrE)CbA%Tj;)-Nrbgm?DRjnQ(X5maT0P2|3(9x>YS}Q zmhE}Wwm+OJrob4ui>dE3(`qA|wm#$@pc1)8PtOF#?5SP;htVe=R8Srrj?|sBD)PZl zP!tZ>fW>_AX1#5FFYZ)n>Az~hk@}qL%+%e87bvTX%Sf}2_*!cYY=Huex-Pu2{H#gm{m+!Y* z_rd@9csKQTyc4)ix9x4{_^l+SoE8AuBb|cAsOoly#7g`_A5#e@E$M7qhty^I%74GU zx-+gZAMdZWf2Gw}F0ztJG3C6X^h#RCGsN~H_f{CUYHh>sf-qkBgZcS~j(W+!m)t8f zZ4xTAj+c?8O?*tvYgwrVGl;wB96Jm%+kzJp2kwsKD9XhM!_0-{A?i$3U_`Ykz=3Q> zswoTiikx%ivfU{|(s!WXOc8nyW1p@s-=4YkPkoBdG1a(J0E@Mtme`#EVFMzxa{V|k z-+H0b(+Gu|r{c22%?sV6^#WQNMy^7>~*3fVQ;a=gXv$2SaH+kk3-aiJs}o8}7VyXVUT&}^;4mBoVqthv^0jDR= GC*J_5c#cp2 literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Invoice.finalize_invoice.1.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Invoice.finalize_invoice.1.json new file mode 100644 index 0000000000000000000000000000000000000000..2c353ac873d8c39a86a50327269729c50ece12d5 GIT binary patch literal 4595 zcmds5S#KLR5PtWsSn#O@nkbTw*m-Iar)iMGY0?yJf`FiQDT@)8T=S4G_`i2}?xB_I zwkTj!4}#1g=bmq7e7#c?h?J$Y!eIZcF42<$efo%g=#m7fDVz(-c{s2Ai^{swN8O`_ zuxfawXib=4;7(@VT9;NSDoXEXtE(u^YETd_80D4jN%aJ(yfCZryOR%ZpT2qZ`Nc6Xopv8n z1-_^h)uZpRivf3BmZav?{CH(KYaW)e9`G23x{M0oW+_E4QkkNctZ9P3TW_UjhFoBJ z;X>+!3&$h6!C4GSy9>-4{pG8tRNyzbJ8)Y?7sPUd8**7w$krT#EHd5D(kh=28x$u> z0e9!2B#i1(vBrfJ4%-(@`*P@=S&E}>`5qM&7>?(J$0Q8Juwa64c10^poY@0Y7E&4? z(adHkHRK9rVl7!ou~i{b#x(lLWCA8X)CRPqE?$#(YXWs^`uPAE;i!7!I3*xlI6pc% zcojn6D-|&@WRh;j^h#Pa)dFlm%l}NPiId<{0IJsq=^8)?5DL6bK=c>?S5bshPaz(N zfHpe>t=-%3L1R@GNmVc8=(8(?;-ev=A}W+)x)_sy%y|!AB00K`-R{10Ifj-2x;Zbh zyFKG0?=iH8Cp(J|LI9n0nX8^Dy4;k<~wzz(^gih1m6y$BwLJcbw3-Pz%Ee|osL z-#!4MHbnU}-kt65J>K2h+s}UP3T~hVfJsW~Xn3O_qv5zGjsT9XM~nht36^!aq`v7O z&=_06j8Bcg74B`r*Y?5)&SqkWo!1RdU#@?dT^-MUKU|+S&)&T}CT}0}Z|rGb-u6g< zGF-})ZSaaZRPQ?gs9Pch6v4{X0C2O4Z5)FOdm(9*tZay~`-y-4baGaoAI*=<>bLVy z-kFi2t{D3GoQZ}}rEyHL4b6=rLNBDM@tGyUFf(eOxoli45nwwu41(5E)rAh{I)yUb zM#U8^Sq%*sSiv3Yw$*VnfCL2ZTneWXi0uAhw==`wjd3I8WZcc$Ji=pX0n zPWT*{ZS6Vt1?j}==TgxRCvjL0MG4B?E2JCNE$G*Q!YuR1Xyzxc#HK+{xi0eOUFb(vITc_TSj5LIYO%>Y2EV796@?`DDUpGQ) zn1i>@Ws@*of)?J>qY1wWI!MrqcKsk>A?>t#By2qoi&|XL9sr8O8M=k42#>zxP~4XD zMw^x!CJgL2UFKnI!*Nc_@Z=oe1|%5i@FsW#D(n_{Yl5k%f_5xm0nu$!4ogd;=$-CC zl~LH3aJDy%4Pj6m+lN3kn0wvYmWFTXH`4wxTgD1ke?Cn?#o$<&FuI|1 u0|$hx97K5V#wYsNY=Xj2219Y2{*xK#oX(dph-z@M!*B@o(4?K4oj(DFV){P- literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Invoice.finalize_invoice.2.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Invoice.finalize_invoice.2.json new file mode 100644 index 0000000000000000000000000000000000000000..02e6c3881497a859ee19fad1b2ba9b324afe6d0f GIT binary patch literal 3391 zcmcImOK%%D5Wf3Y2s}4YCE2i=Qy(tk0Ih{44N@d12x^zI7;#C8581Ne|K1sr%hj%= zAeZVO*)zj=&o}dYF&Yt8RXQk7lNvbuo_nb12Y`|W{q}-<{vk|XTthDyA zqD9SGJLnZE)J}_v(_mAjyf^O6cnl%Gb{@RsSg&!G8p8zpem_7*AlDs{bN|85^x@NX z|235$jw=yrsHByiX0hy*saFsSS^sZdjnP7E0hr!cq-TI5z$(Z(1JhsPf1^>NMh=UR z1iTK8kPdH7jag>Zy_%EHI0>TYtvA8Y4M%|QCjhu-Dg_)Rs?7jcaGbTSs$?9eM68Q*NbgL$`q`FZ{{){N7+K3BWw{0*cmi z6-!6U$l?KUj!kP`i3Vm6bfFwM-DXw;oG0iWj^u%tvx6QcE-X(We#!#D4Urt|cEkaz zzdTw-jydDA%GoXM9S)AH@TmtD|z=+ZL^MubTmU- z-=&0tv>@An3!Ayz8~`*mbg+W;?Ycfrkr~UsOAbbm!@WUECfZ3$*m~-k0M}qu;L+1= zE*T^393DRVF9M2ru7?yutY~h zIA!Yg07BW)P-i9(Co?uMIM;1+oJb(F#^QiN#i~^Kj*Oi@co>J_)IgJjWw1-~(kyBo V_;L;S?I6+E_E7HO0eA6o@gHb*%0d7D literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Invoice.finalize_invoice.3.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Invoice.finalize_invoice.3.json new file mode 100644 index 0000000000000000000000000000000000000000..ae889f9cd979e6259862e7d97922bc5e3ad96942 GIT binary patch literal 3391 zcmcImOK%%D5Wf3Y2zzd#$g-O_r>01dzHBEk3=|0pg4(4l)?9MUhb-IhfA2RWcUQZT zf?TSDWX}xeJ>SgJ(PUEavNS<^7QgL7@pe*NUll)Xvox;-=sGBs=yiNi1)?6md4^JO(A3m!+vkI>tpWvjePI89k^}$8js!G%4SGw%TW& zZF(&i#jBuuV6*A=(b>wz!b0+1XOrFHB zTH%~e3815d${%~Chc-`MQq`t$ts{qf&#);GR7JMC)y z`TlbAXWHnUA}c5rL#XbUUJC12Zm}KDz2(|1jjh>T;Mz;SH$MN+P%R6h>$*FWw!y)z58+&oR`v3egrTP9?FU0V(>9P|~&IPH$MozTLBrQfA z>P0$c@*(pA{JdxSm~tj#^A!24?CJ|bx&Wm1I0UFe8kw^Cx}V%n`hyPQDr6lI=~#xg zp-Tw`S%+*RE;e&_a{y>6bg;(yc6}eG$QmntNDdjLi)A|h|D$x!R zr%crzKqT8W)PV`4$&3vQ=lX3<6A40VEDk6tR;J4LWa|9U!#E842D(UChFy}EW~b&} UELOm8heT7`quk>I?&$gGKLe=BegFUf literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Invoice.list.1.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Invoice.list.1.json new file mode 100644 index 0000000000000000000000000000000000000000..f5030262039ea4b90e4fbb32a23ce88f418f1670 GIT binary patch literal 9185 zcmeHMS#uOO5Ps)ZY|T?r6>xUfCr<%Pz?FarM@XP3m3Bt!6+5FDYc#vq6#qTll19>8 zYlqmBE7%VPrdzF6A73AQzB3v{2~{+@KY9;;AB{fi-|&^jab5{U^ly0v4AGlc(NAr% zpcw<2sM6HaGJP_skTTi-8GhQdg7O52o$r!mlnX9SdQKKa%44c{F36m9FI9?Vh1Ity z+E5r?IHzgJyfT%_2~Ad1#3)b6%F|{%O^qH=yrA-g5enQH+#l04J@b~Zj6^b8<^se- z85I>xJ%>3Dl!le!JWW}wpOZ1Q$Wgruu8QFfrQb0|qv{{`n7ERXiCBB8DwE*)sszQ! zoU=43J6dQWlltSXME zuNbj9H~5)Iz(8b1Qybqt+vZ%*l%KPNP&7}^Q|EcEbW}7%F7guMk8rWddCW*9ji;3= z%KKYeV3vEO0__?4d&_cd0g|X@TtQ?Zwd{PhF%5rA)^AVGK6;mFxQMuD3#v~{0#>VC zk<8(3(c;>)+rk@M*Z^8+XVwVXMpi^ukFVxm5!Fx$YBpigQioF7l9NWPMDH-uMz0hV z36;s{?g-$3tpgyckJ&h)vlQdJ0g0ByTAL49d+MHDAFwv(0K#Z=e)*#*U$HDthE#zZ zqqahQ{!$y;-kQ%o0)pawXndkCZ?P0iat*ECE%q>^2b>&FcMkUUwkO*=&4b{*Qc;O%?~M1icWm)90@}(HkY*q-%olr8At70S)KP%~{I3Qc4hqzjrHmI$SJ?<&Ln^^l z%lyE*sWcchkZGVqI0oZ)tuGlrS^Y7c9!~z+U!4>WpFf$rKAJu~EKSbPiOLY|4=+CeGWT; zlA)=(<;5{5&SXh!*!I3iNh->DE;DjkQK7gRe7ia;Fa{zhQABZYbVq%3IdM>d4b>(V zlCYR(koO=Hd85r?D|0AArrh*FL7@G#l_ODE&4#L#u~dXr&M4lja{;+q1Z$$Ti>)Jl z*$sv{h3aSFD(%GV@!n*Ac_JGo_~OjrN8<-sTZkS(bROvVC=DX zN@OeJ)=C#TES4oyydgN9Y?^*SY0Gt*hHerW~S-0 z4c}eh32LZ+v$ehp%E-3vjaY$?rX%7)L1ToGS8(HvZI2*tC-e0Ad3+kk(&$Vq6nE7$ z=uI3F_i#W*;`%WF!2~izK(Kmdb2j5te$S#D~>PIVcsw={)Fff1^$5`kI0Q10!CQV_#!Zd@>X*a|Ar8XIBvtQX~vCj8!Bo2sIF%35cUL0?OVYr#! zx6|D{d%bFf69VqQ>cm;{Mk2EK?GY@YULJS0_BS1o`|tvE9sp&- z9fDF%=w5y?8EoFry*G0&8HVj{x_n&zmdrv6T-#o9;!*rx?j^_9yO(UNW89uC3O16y zQw|#4p^ao?n^6y?ZXwyY%*RKBY1qu-o;-QqDg d{|$DIP}4u4J9__HB1i@I9}<0u9o_kI=O3PfXJP;V literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Invoice.list.2.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Invoice.list.2.json new file mode 100644 index 0000000000000000000000000000000000000000..ad6f6b03658c3c683aaa31035d147994ab70db58 GIT binary patch literal 13049 zcmeHN>vPjM5dY3!k@2UynS+v~Pd*(`?&xuCcpi5!j7PDRM#Pp~Nlw}_{O`AVSP$Dt z3$#q3>4#FRm9*N&Z+Er&bf?#gQY29Hp!XjB?)5&Y-|!VBNl{S|tKaMl2%K36=cVB{ zTw0G{x#0w596M$zQN$>nBbvzd1gqTGm}HseBTA--9pe}Qbq@O{D9g^ABn)B9FyaM; zWuhF>3T2K$&xesfN=cDrI8n#Rv6vQ;y$iNVU=N|r(U(G1KlU(5#TcfE(p1G$Z~KY^ zAouMMaf&CX%0yhEvmAqOSiJ=-)Kt8vn6j(RQMomPounlF%`s0HDWzG!Ksb|d z<&3TG6^;$h0X?QE2q+$-%-FZ{wh5sqBj-4ckrYqIQ71(qlvM;vP74n97ZW-!NP^>v zX-x}J@&_9mAeMVv0QXqxd&5v|0FsE%uE4V7C~ti>HVyv_+1}`I;8dpIA|kZ8P(5Sl zks9vGbOL9Krq{;ZhTOrL4WNZmW{sdtWR>XZ@KygWLJBH@%@8Kd=rWf!!)ahE(L3pB zy;lO!6tT2-w+C>57akDR(~KRVaVG6NfJ8%Lq)nDdd($4fo?v870EDH`$>pbJWRCM9 zUB(Ld7%~y+)8|^)X4hi;5fD`FCxsuY%^M^oW<)`&Q;V4l?g2CV!@d5ne}AyK6;1^0 zaeX$;y3R9Ie#(S+8_M8J3lTT9sPNF$`8d~=O^vGS8EXV z&5B}HmGKPA;M5inP^{20j8c-!I%0)o+u4B@bxV~cE7D3^iu^Nv`et-Oc}`y}MyJnQ zR$~`rSc8wF7F1xoIvz5aFw-e_T`(ShXX#{_C@g7io=F{FtI6;>SP_x7&+_82=`qt5`{pS z&X7UaeW#SjM8=J|3Uo49a)@}#;B>g5gNNbFA`8G;H#Tq3+B^`qn5Tle!1C0v~loM5utoNLpS=v^I7ef;^{BXoX)@-dYNs zV*|x)HZ^+37KuA3AS1E)7=U03o1JR$&16`e)Hq`;_H|vnu8Y@ovBu##-bHoATStL)U3_hI z@nG@xE-|i=kIn7jHTQc{`Y7N9T#VS(EpC zaBI1)i`R8A;LYu-i`{LFzBhym8zWc3WZ}2L*#6criwtc-Oad_{w?h>!N#O6}6 z&Xd>G@b~Ky_ZNG6+Xt7ghP?+3b!B#qU1H7Ml~=>Hq3Klgf~Fo=qu-)wG>NU`%v$NU zw}zdF&6>5KFK!LDrmilax|<+oSC^=LLuY~eU}-Jg)Dk#f?2Y%t?4N~$7R&HWFM(UO>NSrWongN#+Uwb_qwyeUl>c0f zVc%jO{c-Smvb{a?MGy#}?)MmB*yj6P)y4{i{DlzwT=z z&(g6kfor>$95@vJ!uLA;>%Er@o+;S(Cz1D3$7RZ1ugAc;b@}?GPH>x1w`lD{onW)| zy`ypy`yNN#J=JC+8(;8F=exhhX+PfrSH7xOIqv4{>mKBI^S{Ge9Ej;3q&hmE>v^tV OAL?~Ec68_So&N!lsBIbm literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--InvoiceItem.create.1.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--InvoiceItem.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..5c373505530cd1f175fd4e242e5021353d9ceb11 GIT binary patch literal 1008 zcmZvb+invv5Qgvj6f0kW1kz^Ji;6pFtAc>0R6^pQ%5v;U8RE4!J}iW&@6OoXO*XK( z+RWjf&;Qu_v#MgEwb2Cjs(RU6)OGzL#bk62jNDO_MX%Tp^ue~^Qcy7=(7+C!E?l@t(9r;pR>x+(o=dW$l(ny7W|)YwC7)u~(yK@hW`!u?Y&v#XyVlO1f( zzyNI#?ZBZ;^Y`QtxkTjj=k%-;_CdHLa7<8n5%07}4jh;|S0D#LAp=2D!Qs3lw?eJS zAnxT4Xj76A2ag6rrlJbzfNY;-nOh`v6c_V!zPRZ>HGi&}FBkn|_vZFy>+aU@^;bWt z&-q3Y+S*O*_y*D?84pD9{~V>5LAfy^!%r%YP8S=J{Z!mJt76$e`s8mvFSq9XrFC$> z6ndm`kd8*aoK6o2&huf-lUczDe%c82+ISyDnR64e)?OI>02G`OFI;&Pj?Z zkk}F&EvLL9c3io$P7F8yuvRvqSmbNx<7)awMsya$Z%+N-N&EXlE}aaf;N;a#5_3dP GXMX{7Rvo?o literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--InvoiceItem.create.2.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--InvoiceItem.create.2.json new file mode 100644 index 0000000000000000000000000000000000000000..131e4a36076d0bf1740634ab1229b2fad3c99637 GIT binary patch literal 981 zcmZuw+fExX5Pjz>B0u*bn~Mm2qe?}Q3aG7GRaBMb^$v81*Is-Hr4j$mjQ7H(NuD;x zbIzPOWA9ET6QOFCY!vS&v(C&Xz@y{-I|_wj0n2}i z`D|4zi`8Pue1bz%o(1S&xj3CK7E8ig@x~|~dGOTu)?_EBbR?5hFw9GGB^rbFUIzUK zYCaU?LnezcyP^v1fpX4Xh9EjG$2LcdHpSds78lzuvw!Q^&(-$6`EY%)R#&Iy0ndB& zW4V!pac+|uxdFZv{Xm2tBubb?y|F5rA6333-Nw*aZoI2fy8-@`pT6Hb)=y{US={`1 zy6jzf;L#|T{q7OL2idK8)GK;r!=3ZBd`!wljQ_cg&rOcjS&$LQX=jFGmcxsn2XTnY z3N+Tr@Mfq$ye->1uewV<6h)_7Q>4O&+rY@bcbAeCxwcOB zaA$UQmfT-t856CICa||zQO)psMVO4vDI=c{Wzjcm0DZ8nasxu{d>1uz+1DUUBV3cc&ajvIBlI8vUXmkGYm(Xs?Nw5N zhR6WNjSr1xv$E%d*;$Z8HBsxt)UZ>nZI0zy6$IK9Sh#;qWw!pUBqhNH4G603MLXrd zQvW^1g8?Z+fAVti=JIl0%ooH**azWAATNt5U*y$%fp{<8X+gt}o;uf?90Y|7Xc7y? zc~Pzgt-(BSFMp|)4jKC3(LhM9utGY8i6<{(5QCR)q$FToym7-9vYE zdtH3HDOYRX&y2O_awFK+sa2XsuB^06HDa zP|SRM9P~C$4|t;_w0LuIHB`Y~*6pkl!-XGL$|erVbZ3~4_4I8Ve6t|_auiS=rSE@I OXLjDu6h5`{eDMz(`WpWL literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--InvoiceItem.create.4.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--InvoiceItem.create.4.json new file mode 100644 index 0000000000000000000000000000000000000000..594d8b35eefe1f0827d509d92351a98d322f6c8b GIT binary patch literal 996 zcmZvb+iu%142JLf6o#%hAkG18cekNKcWQ&KC@^#w1Vv^$F0$n4kfsaz?jvP8b(ZML zrpTY4eoEVuq7bTf$wu+67@v)3z2TTn-h!TYVaJ?Dah;j_pA5cKHq=5 zpt7>fsZ<2oFIg$CTv*-H7Ucw0~}cv7a5z@V8(EeuONC=(#7 zV8z9V`;GfXqtUdjN4s{Yxzr|Oa;3pF)UMi>t05}FRg}1W$!Tu!2Q)h&It`3ax1tSr zbliVS_K-nf`KO$mmy>cdp3b^QIsUnJmUkMH$)yIi6W|MszI4W=IvSi@CcjFV{Dd*~R$B+4`~h zaDBPigsb@F@2Xcn<{L>E=a#9FOW^y_ABf@yj#A8`URssm2br%EUhjdxXQC%~Wb z)7^Y!Lv3%K=9{m#J(UMO8u_x{JtFuZyD<-XMXzjlaK4tSq-@0aA9Z|gvbD~Fj7X#% z4BISskAvUF;fAb0V@)@gM#CB6b==;0)m`}ANX39)lJATNX)$~oJ-3KzBS`=PN&o!C OoyBpWVfYZ|<>VhX(i~9$ literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Token.create.1.json b/corporate/tests/stripe_fixtures/switch_from_monthly_plan_to_annual_plan_for_manual_license_management--Token.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..60d47536e7c17c03b5152a712f27efc6362cb64e GIT binary patch literal 826 zcmaJe?5488L!s+`eO3a8zWw!@@-X^0(msZ3&4k2b9m7p)-v`{F>^Rw2?$YCk`} z7kfWS5~aZzb(`$NJ^X<&4!zGcaTlWM6*R3$tMMRRx0YQ*EG;pbWWm7bhq#g~PK0M^ z4Z4I2OgzH)A}rSh>WzR~8?R_C;j)0RZCMT!dFg*SwWCH@r*ejjA$O(k%7K-~QSaoT zFV%m%FV4ZOzQSWV3l-_S1kbboBJsP)tJ-oA!qbx{iFF7rgtBznN7rOO(KU`Ni=^k5 zx2O5z!^izyDreFwS+~{%q(HPoL)<4QI+1bbgW5>&m!sFIX4x$3>nmh1zz~@Chm+D^ zOCDX#4af#K*MsZT7wV`p?$-&<^#(2NfT^~5;45m;B-0G*6*-kI2`%Txk)=2Bi=^Tv zj93aLz2H=M)zMhszV1tvw8g46B6tm@$5{L;G>SX6;;xCd&e~P$W9L{mI*fh*S9axJ literal 0 HcmV?d00001 diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index b1078415c4..3bd16fb407 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -1322,6 +1322,245 @@ class StripeTest(StripeTestCase): invoice_plans_as_needed(self.next_year + timedelta(days=400)) mocked.assert_not_called() + @mock_stripe() + @patch("corporate.lib.stripe.billing_logger.info") + def test_switch_from_monthly_plan_to_annual_plan_for_automatic_license_management(self, *mocks: Mock) -> None: + user = self.example_user("hamlet") + + self.login_user(user) + with patch('corporate.lib.stripe.timezone_now', return_value=self.now): + self.upgrade(schedule='monthly') + monthly_plan = get_current_plan_by_realm(user.realm) + assert(monthly_plan is not None) + self.assertEqual(monthly_plan.automanage_licenses, True) + self.assertEqual(monthly_plan.billing_schedule, CustomerPlan.MONTHLY) + + response = self.client_post("/json/billing/plan/change", + {'status': CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE}) + self.assert_json_success(response) + monthly_plan.refresh_from_db() + self.assertEqual(monthly_plan.status, CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE) + with patch('corporate.views.timezone_now', return_value=self.now): + response = self.client_get("/billing/") + self.assert_in_success_response(["be switched from monthly to annual billing on February 2, 2012"], response) + + with patch("corporate.lib.stripe.get_latest_seat_count", return_value=20): + update_license_ledger_if_needed(user.realm, self.now) + self.assertEqual(LicenseLedger.objects.filter(plan=monthly_plan).count(), 2) + self.assertEqual(LicenseLedger.objects.order_by('-id').values_list( + 'licenses', 'licenses_at_next_renewal').first(), (20, 20)) + + with patch('corporate.lib.stripe.timezone_now', return_value=self.next_month): + with patch("corporate.lib.stripe.get_latest_seat_count", return_value=25): + update_license_ledger_if_needed(user.realm, self.next_month) + self.assertEqual(LicenseLedger.objects.filter(plan=monthly_plan).count(), 2) + customer = get_customer_by_realm(user.realm) + assert(customer is not None) + self.assertEqual(CustomerPlan.objects.filter(customer=customer).count(), 2) + monthly_plan.refresh_from_db() + self.assertEqual(monthly_plan.status, CustomerPlan.ENDED) + self.assertEqual(monthly_plan.next_invoice_date, self.next_month) + annual_plan = get_current_plan_by_realm(user.realm) + assert(annual_plan is not None) + self.assertEqual(annual_plan.status, CustomerPlan.ACTIVE) + self.assertEqual(annual_plan.billing_schedule, CustomerPlan.ANNUAL) + self.assertEqual(annual_plan.invoicing_status, CustomerPlan.INITIAL_INVOICE_TO_BE_SENT) + self.assertEqual(annual_plan.billing_cycle_anchor, self.next_month) + self.assertEqual(annual_plan.next_invoice_date, self.next_month) + self.assertEqual(annual_plan.invoiced_through, None) + annual_ledger_entries = LicenseLedger.objects.filter(plan=annual_plan).order_by('id') + self.assertEqual(len(annual_ledger_entries), 2) + self.assertEqual(annual_ledger_entries[0].is_renewal, True) + self.assertEqual(annual_ledger_entries.values_list('licenses', 'licenses_at_next_renewal')[0], (20, 20)) + self.assertEqual(annual_ledger_entries[1].is_renewal, False) + self.assertEqual(annual_ledger_entries.values_list('licenses', 'licenses_at_next_renewal')[1], (25, 25)) + + invoice_plans_as_needed(self.next_month) + + annual_ledger_entries = LicenseLedger.objects.filter(plan=annual_plan).order_by('id') + self.assertEqual(len(annual_ledger_entries), 2) + annual_plan.refresh_from_db() + self.assertEqual(annual_plan.invoicing_status, CustomerPlan.DONE) + self.assertEqual(annual_plan.invoiced_through, annual_ledger_entries[1]) + self.assertEqual(annual_plan.billing_cycle_anchor, self.next_month) + self.assertEqual(annual_plan.next_invoice_date, add_months(self.next_month, 1)) + monthly_plan.refresh_from_db() + self.assertEqual(monthly_plan.next_invoice_date, None) + + invoices = [invoice for invoice in stripe.Invoice.list(customer=customer.stripe_customer_id)] + self.assertEqual(len(invoices), 3) + + annual_plan_invoice_items = [invoice_item for invoice_item in invoices[0].get("lines")] + self.assertEqual(len(annual_plan_invoice_items), 2) + annual_plan_invoice_item_params = { + "amount": 5 * 80 * 100, + "description": "Additional license (Feb 2, 2012 - Feb 2, 2013)", + "plan": None, "quantity": 5, "subscription": None, "discountable": False, + "period": { + "start": datetime_to_timestamp(self.next_month), + "end": datetime_to_timestamp(add_months(self.next_month, 12)) + }, + } + for key, value in annual_plan_invoice_item_params.items(): + self.assertEqual(annual_plan_invoice_items[0][key], value) + + annual_plan_invoice_item_params = { + "amount": 20 * 80 * 100, "description": "Zulip Standard - renewal", + "plan": None, "quantity": 20, "subscription": None, "discountable": False, + "period": { + "start": datetime_to_timestamp(self.next_month), + "end": datetime_to_timestamp(add_months(self.next_month, 12)) + }, + } + for key, value in annual_plan_invoice_item_params.items(): + self.assertEqual(annual_plan_invoice_items[1][key], value) + + monthly_plan_invoice_items = [invoice_item for invoice_item in invoices[1].get("lines")] + self.assertEqual(len(monthly_plan_invoice_items), 1) + monthly_plan_invoice_item_params = { + "amount": 14 * 8 * 100, + "description": "Additional license (Jan 2, 2012 - Feb 2, 2012)", + "plan": None, "quantity": 14, "subscription": None, "discountable": False, + "period": { + "start": datetime_to_timestamp(self.now), + "end": datetime_to_timestamp(self.next_month) + }, + } + for key, value in monthly_plan_invoice_item_params.items(): + self.assertEqual(monthly_plan_invoice_items[0][key], value) + + with patch("corporate.lib.stripe.get_latest_seat_count", return_value=30): + update_license_ledger_if_needed(user.realm, add_months(self.next_month, 1)) + invoice_plans_as_needed(add_months(self.next_month, 1)) + + invoices = [invoice for invoice in stripe.Invoice.list(customer=customer.stripe_customer_id)] + self.assertEqual(len(invoices), 4) + + monthly_plan_invoice_items = [invoice_item for invoice_item in invoices[0].get("lines")] + self.assertEqual(len(monthly_plan_invoice_items), 1) + monthly_plan_invoice_item_params = { + "amount": 5 * 7366, + "description": "Additional license (Mar 2, 2012 - Feb 2, 2013)", + "plan": None, "quantity": 5, "subscription": None, "discountable": False, + "period": { + "start": datetime_to_timestamp(add_months(self.next_month, 1)), + "end": datetime_to_timestamp(add_months(self.next_month, 12)) + }, + } + for key, value in monthly_plan_invoice_item_params.items(): + self.assertEqual(monthly_plan_invoice_items[0][key], value) + invoice_plans_as_needed(add_months(self.now, 13)) + + invoices = [invoice for invoice in stripe.Invoice.list(customer=customer.stripe_customer_id)] + self.assertEqual(len(invoices), 5) + + annual_plan_invoice_items = [invoice_item for invoice_item in invoices[0].get("lines")] + self.assertEqual(len(annual_plan_invoice_items), 1) + annual_plan_invoice_item_params = { + "amount": 30 * 80 * 100, + "description": "Zulip Standard - renewal", + "plan": None, "quantity": 30, "subscription": None, "discountable": False, + "period": { + "start": datetime_to_timestamp(add_months(self.next_month, 12)), + "end": datetime_to_timestamp(add_months(self.next_month, 24)) + }, + } + for key, value in annual_plan_invoice_item_params.items(): + self.assertEqual(annual_plan_invoice_items[0][key], value) + + @mock_stripe() + @patch("corporate.lib.stripe.billing_logger.info") + def test_switch_from_monthly_plan_to_annual_plan_for_manual_license_management(self, *mocks: Mock) -> None: + user = self.example_user("hamlet") + num_licenses = 35 + + self.login_user(user) + with patch('corporate.lib.stripe.timezone_now', return_value=self.now): + self.upgrade(schedule='monthly', license_management='manual', licenses=num_licenses) + monthly_plan = get_current_plan_by_realm(user.realm) + assert(monthly_plan is not None) + self.assertEqual(monthly_plan.automanage_licenses, False) + self.assertEqual(monthly_plan.billing_schedule, CustomerPlan.MONTHLY) + + response = self.client_post("/json/billing/plan/change", + {'status': CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE}) + self.assert_json_success(response) + monthly_plan.refresh_from_db() + self.assertEqual(monthly_plan.status, CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE) + with patch('corporate.views.timezone_now', return_value=self.now): + response = self.client_get("/billing/") + self.assert_in_success_response(["be switched from monthly to annual billing on February 2, 2012"], response) + + with patch('corporate.lib.stripe.timezone_now', return_value=self.next_month): + invoice_plans_as_needed(self.next_month) + + self.assertEqual(LicenseLedger.objects.filter(plan=monthly_plan).count(), 1) + customer = get_customer_by_realm(user.realm) + assert(customer is not None) + self.assertEqual(CustomerPlan.objects.filter(customer=customer).count(), 2) + monthly_plan.refresh_from_db() + self.assertEqual(monthly_plan.status, CustomerPlan.ENDED) + self.assertEqual(monthly_plan.next_invoice_date, None) + annual_plan = get_current_plan_by_realm(user.realm) + assert(annual_plan is not None) + self.assertEqual(annual_plan.status, CustomerPlan.ACTIVE) + self.assertEqual(annual_plan.billing_schedule, CustomerPlan.ANNUAL) + self.assertEqual(annual_plan.invoicing_status, CustomerPlan.INITIAL_INVOICE_TO_BE_SENT) + self.assertEqual(annual_plan.billing_cycle_anchor, self.next_month) + self.assertEqual(annual_plan.next_invoice_date, self.next_month) + annual_ledger_entries = LicenseLedger.objects.filter(plan=annual_plan).order_by('id') + self.assertEqual(len(annual_ledger_entries), 1) + self.assertEqual(annual_ledger_entries[0].is_renewal, True) + self.assertEqual(annual_ledger_entries.values_list('licenses', 'licenses_at_next_renewal')[0], (num_licenses, num_licenses)) + self.assertEqual(annual_plan.invoiced_through, None) + + with patch('corporate.lib.stripe.timezone_now', return_value=self.next_month): + invoice_plans_as_needed(self.next_month + timedelta(days=1)) + + annual_plan.refresh_from_db() + self.assertEqual(annual_plan.invoiced_through, annual_ledger_entries[0]) + self.assertEqual(annual_plan.next_invoice_date, add_months(self.next_month, 12)) + self.assertEqual(annual_plan.invoicing_status, CustomerPlan.DONE) + + invoices = [invoice for invoice in stripe.Invoice.list(customer=customer.stripe_customer_id)] + self.assertEqual(len(invoices), 2) + + annual_plan_invoice_items = [invoice_item for invoice_item in invoices[0].get("lines")] + self.assertEqual(len(annual_plan_invoice_items), 1) + annual_plan_invoice_item_params = { + "amount": num_licenses * 80 * 100, "description": "Zulip Standard - renewal", + "plan": None, "quantity": num_licenses, "subscription": None, "discountable": False, + "period": { + "start": datetime_to_timestamp(self.next_month), + "end": datetime_to_timestamp(add_months(self.next_month, 12)) + }, + } + for key, value in annual_plan_invoice_item_params.items(): + self.assertEqual(annual_plan_invoice_items[0][key], value) + + with patch('corporate.lib.stripe.invoice_plan') as m: + invoice_plans_as_needed(add_months(self.now, 2)) + m.assert_not_called() + + invoice_plans_as_needed(add_months(self.now, 13)) + + invoices = [invoice for invoice in stripe.Invoice.list(customer=customer.stripe_customer_id)] + self.assertEqual(len(invoices), 3) + + annual_plan_invoice_items = [invoice_item for invoice_item in invoices[0].get("lines")] + self.assertEqual(len(annual_plan_invoice_items), 1) + annual_plan_invoice_item_params = { + "amount": num_licenses * 80 * 100, + "description": "Zulip Standard - renewal", + "plan": None, "quantity": num_licenses, "subscription": None, "discountable": False, + "period": { + "start": datetime_to_timestamp(add_months(self.next_month, 12)), + "end": datetime_to_timestamp(add_months(self.next_month, 24)) + }, + } + for key, value in annual_plan_invoice_item_params.items(): + self.assertEqual(annual_plan_invoice_items[0][key], value) + @patch("corporate.lib.stripe.billing_logger.info") def test_reupgrade_after_plan_status_changed_to_downgrade_at_end_of_cycle(self, mock_: Mock) -> None: user = self.example_user("hamlet") @@ -1719,7 +1958,8 @@ class LicenseLedgerTest(StripeTestCase): self.assertEqual(LicenseLedger.objects.count(), 1) # Plan needs to renew # TODO: do_deactivate_user for a user, so that licenses_at_next_renewal != licenses - ledger_entry = make_end_of_cycle_updates_if_needed(plan, self.next_year) + new_plan, ledger_entry = make_end_of_cycle_updates_if_needed(plan, self.next_year) + self.assertIsNone(new_plan) self.assertEqual(LicenseLedger.objects.count(), 2) ledger_params = { 'plan': plan, 'is_renewal': True, 'event_time': self.next_year, diff --git a/corporate/views.py b/corporate/views.py index 1ff800b485..b49f19083d 100644 --- a/corporate/views.py +++ b/corporate/views.py @@ -200,14 +200,17 @@ def billing_home(request: HttpRequest) -> HttpResponse: plan = get_current_plan_by_customer(customer) if plan is not None: now = timezone_now() - last_ledger_entry = make_end_of_cycle_updates_if_needed(plan, now) + new_plan, last_ledger_entry = make_end_of_cycle_updates_if_needed(plan, now) if last_ledger_entry is not None: + if new_plan is not None: # nocoverage + plan = new_plan plan_name = { CustomerPlan.STANDARD: 'Zulip Standard', CustomerPlan.PLUS: 'Zulip Plus', }[plan.tier] free_trial = plan.status == CustomerPlan.FREE_TRIAL downgrade_at_end_of_cycle = plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE + switch_to_annual_at_end_of_cycle = plan.status == CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE licenses = last_ledger_entry.licenses licenses_used = get_latest_seat_count(user.realm) # Should do this in javascript, using the user's timezone @@ -226,6 +229,7 @@ def billing_home(request: HttpRequest) -> HttpResponse: 'free_trial': free_trial, 'downgrade_at_end_of_cycle': downgrade_at_end_of_cycle, 'automanage_licenses': plan.automanage_licenses, + 'switch_to_annual_at_end_of_cycle': switch_to_annual_at_end_of_cycle, 'licenses': licenses, 'licenses_used': licenses_used, 'renewal_date': renewal_date, @@ -244,7 +248,8 @@ def billing_home(request: HttpRequest) -> HttpResponse: @has_request_variables def change_plan_status(request: HttpRequest, user: UserProfile, status: int=REQ("status", validator=check_int)) -> HttpResponse: - assert(status in [CustomerPlan.ACTIVE, CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE, CustomerPlan.ENDED]) + assert(status in [CustomerPlan.ACTIVE, CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE, + CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE, CustomerPlan.ENDED]) plan = get_current_plan_by_realm(user.realm) assert(plan is not None) # for mypy @@ -255,6 +260,11 @@ def change_plan_status(request: HttpRequest, user: UserProfile, elif status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE: assert(plan.status == CustomerPlan.ACTIVE) do_change_plan_status(plan, status) + elif status == CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE: + assert(plan.billing_schedule == CustomerPlan.MONTHLY) + assert(plan.status == CustomerPlan.ACTIVE) + assert(plan.fixed_price is None) + do_change_plan_status(plan, status) elif status == CustomerPlan.ENDED: assert(plan.status == CustomerPlan.FREE_TRIAL) downgrade_now(user.realm) diff --git a/templates/corporate/billing.html b/templates/corporate/billing.html index 5952d6fad4..440b743624 100644 --- a/templates/corporate/billing.html +++ b/templates/corporate/billing.html @@ -51,6 +51,8 @@ ${{ renewal_amount }}. {% elif downgrade_at_end_of_cycle %} Your plan will be downgraded to Zulip Limited on {{ renewal_date }}. + {% elif switch_to_annual_at_end_of_cycle %} + Your plan will be switched from monthly to annual billing on {{ renewal_date }}. {% else %} Your plan will renew on {{ renewal_date }} for ${{ renewal_amount }}. diff --git a/zerver/models.py b/zerver/models.py index acc9c276b7..a944aa0d4c 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -2674,6 +2674,7 @@ class AbstractRealmAuditLog(models.Model): CUSTOMER_CREATED = 501 CUSTOMER_PLAN_CREATED = 502 + CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN = 503 event_type: int = models.PositiveSmallIntegerField()