From f1b1bf5a0d856c2f73e8fedbc548d095b3c848e4 Mon Sep 17 00:00:00 2001 From: Vishnu KS Date: Thu, 23 Apr 2020 23:40:15 +0530 Subject: [PATCH] billing: Add support for Zulip Standard free trial. --- corporate/lib/stripe.py | 110 +++--- corporate/models.py | 1 + ..._trial_upgrade_by_card--Charge.list.1.json | Bin 0 -> 82 bytes ...al_upgrade_by_card--Customer.create.1.json | Bin 0 -> 2000 bytes ..._upgrade_by_card--Customer.retrieve.1.json | Bin 0 -> 2635 bytes ..._upgrade_by_card--Customer.retrieve.2.json | Bin 0 -> 2635 bytes ...ial_upgrade_by_card--Invoice.create.1.json | Bin 0 -> 2528 bytes ...ial_upgrade_by_card--Invoice.create.2.json | Bin 0 -> 2539 bytes ...ial_upgrade_by_card--Invoice.create.3.json | Bin 0 -> 2528 bytes ...e_by_card--Invoice.finalize_invoice.1.json | Bin 0 -> 2741 bytes ...e_by_card--Invoice.finalize_invoice.2.json | Bin 0 -> 2752 bytes ...e_by_card--Invoice.finalize_invoice.3.json | Bin 0 -> 2741 bytes ...trial_upgrade_by_card--Invoice.list.1.json | Bin 0 -> 83 bytes ...trial_upgrade_by_card--Invoice.list.2.json | Bin 0 -> 83 bytes ...trial_upgrade_by_card--Invoice.list.3.json | Bin 0 -> 3211 bytes ...trial_upgrade_by_card--Invoice.list.4.json | Bin 0 -> 3211 bytes ...trial_upgrade_by_card--Invoice.list.5.json | Bin 0 -> 6348 bytes ...trial_upgrade_by_card--Invoice.list.6.json | Bin 0 -> 9474 bytes ...upgrade_by_card--InvoiceItem.create.1.json | Bin 0 -> 517 bytes ...upgrade_by_card--InvoiceItem.create.2.json | Bin 0 -> 536 bytes ...upgrade_by_card--InvoiceItem.create.3.json | Bin 0 -> 517 bytes ...trial_upgrade_by_card--Token.create.1.json | Bin 0 -> 826 bytes ...upgrade_by_invoice--Customer.create.1.json | Bin 0 -> 1199 bytes ...grade_by_invoice--Customer.retrieve.1.json | Bin 0 -> 1199 bytes ...grade_by_invoice--Customer.retrieve.2.json | Bin 0 -> 1199 bytes ..._upgrade_by_invoice--Invoice.create.1.json | Bin 0 -> 2513 bytes ..._upgrade_by_invoice--Invoice.create.2.json | Bin 0 -> 2513 bytes ...y_invoice--Invoice.finalize_invoice.1.json | Bin 0 -> 2720 bytes ...y_invoice--Invoice.finalize_invoice.2.json | Bin 0 -> 2720 bytes ...al_upgrade_by_invoice--Invoice.list.1.json | Bin 0 -> 83 bytes ...al_upgrade_by_invoice--Invoice.list.2.json | Bin 0 -> 3190 bytes ...al_upgrade_by_invoice--Invoice.list.3.json | Bin 0 -> 3190 bytes ...al_upgrade_by_invoice--Invoice.list.4.json | Bin 0 -> 3190 bytes ...al_upgrade_by_invoice--Invoice.list.5.json | Bin 0 -> 6295 bytes ...rade_by_invoice--InvoiceItem.create.1.json | Bin 0 -> 518 bytes ...rade_by_invoice--InvoiceItem.create.2.json | Bin 0 -> 518 bytes corporate/tests/test_stripe.py | 327 ++++++++++++++++++ corporate/views.py | 18 +- templates/corporate/billing.html | 15 +- templates/corporate/upgrade.html | 34 ++ templates/zerver/plans.html | 8 + tools/check-templates | 1 + zerver/views/portico.py | 4 +- zproject/default_settings.py | 2 + zproject/dev_settings.py | 1 + 45 files changed, 474 insertions(+), 47 deletions(-) create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Charge.list.1.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Customer.create.1.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Customer.retrieve.1.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Customer.retrieve.2.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.create.1.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.create.2.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.create.3.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.finalize_invoice.1.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.finalize_invoice.2.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.finalize_invoice.3.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.list.1.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.list.2.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.list.3.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.list.4.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.list.5.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.list.6.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--InvoiceItem.create.1.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--InvoiceItem.create.2.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--InvoiceItem.create.3.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Token.create.1.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Customer.create.1.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Customer.retrieve.1.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Customer.retrieve.2.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Invoice.create.1.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Invoice.create.2.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Invoice.finalize_invoice.1.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Invoice.finalize_invoice.2.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Invoice.list.1.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Invoice.list.2.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Invoice.list.3.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Invoice.list.4.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Invoice.list.5.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--InvoiceItem.create.1.json create mode 100644 corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--InvoiceItem.create.2.json diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index e76135aeff..2bb61b079a 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -85,6 +85,10 @@ def next_month(billing_cycle_anchor: datetime, dt: datetime) -> datetime: 'billing_cycle_anchor: %s, dt: %s' % (billing_cycle_anchor, dt)) def start_of_next_billing_cycle(plan: CustomerPlan, event_time: datetime) -> datetime: + if plan.status == CustomerPlan.FREE_TRIAL: + assert(plan.next_invoice_date is not None) # for mypy + return plan.next_invoice_date + months_per_period = { CustomerPlan.ANNUAL: 12, CustomerPlan.MONTHLY: 1, @@ -237,6 +241,16 @@ def make_end_of_cycle_updates_if_needed(plan: CustomerPlan, 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 @@ -255,7 +269,8 @@ def update_or_create_stripe_customer(user: UserProfile, stripe_token: Optional[s def compute_plan_parameters( automanage_licenses: bool, billing_schedule: int, - discount: Optional[Decimal]) -> Tuple[datetime, datetime, datetime, int]: + discount: Optional[Decimal], + free_trial: Optional[bool]=False) -> Tuple[datetime, datetime, datetime, int]: # Everything in Stripe is stored as timestamps with 1 second resolution, # so standardize on 1 second resolution. # TODO talk about leapseconds? @@ -275,6 +290,9 @@ def compute_plan_parameters( next_invoice_date = period_end if automanage_licenses: next_invoice_date = add_months(billing_cycle_anchor, 1) + if free_trial: + period_end = add_months(billing_cycle_anchor, settings.FREE_TRIAL_MONTHS) + next_invoice_date = period_end return billing_cycle_anchor, next_invoice_date, period_end, price_per_license # Only used for cloud signups @@ -283,6 +301,9 @@ def process_initial_upgrade(user: UserProfile, licenses: int, automanage_license billing_schedule: int, stripe_token: Optional[str]) -> None: realm = user.realm customer = update_or_create_stripe_customer(user, stripe_token=stripe_token) + charge_automatically = stripe_token is not None + free_trial = settings.FREE_TRIAL_MONTHS not in (None, 0) + if get_current_plan_by_customer(customer) is not None: # Unlikely race condition from two people upgrading (clicking "Make payment") # at exactly the same time. Doesn't fully resolve the race condition, but having @@ -293,29 +314,30 @@ def process_initial_upgrade(user: UserProfile, licenses: int, automanage_license raise BillingError('subscribing with existing subscription', BillingError.TRY_RELOADING) billing_cycle_anchor, next_invoice_date, period_end, price_per_license = compute_plan_parameters( - automanage_licenses, billing_schedule, customer.default_discount) + automanage_licenses, billing_schedule, customer.default_discount, free_trial) # The main design constraint in this function is that if you upgrade with a credit card, and the # charge fails, everything should be rolled back as if nothing had happened. This is because we # expect frequent card failures on initial signup. # Hence, if we're going to charge a card, do it at the beginning, even if we later may have to # adjust the number of licenses. - charge_automatically = stripe_token is not None if charge_automatically: - stripe_charge = stripe.Charge.create( - amount=price_per_license * licenses, - currency='usd', - customer=customer.stripe_customer_id, - description="Upgrade to Zulip Standard, ${} x {}".format(price_per_license/100, licenses), - receipt_email=user.delivery_email, - statement_descriptor='Zulip Standard') - # Not setting a period start and end, but maybe we should? Unclear what will make things - # most similar to the renewal case from an accounting perspective. - stripe.InvoiceItem.create( - amount=price_per_license * licenses * -1, - currency='usd', - customer=customer.stripe_customer_id, - description="Payment (Card ending in {})".format(cast(stripe.Card, stripe_charge.source).last4), - discountable=False) + if not free_trial: + stripe_charge = stripe.Charge.create( + amount=price_per_license * licenses, + currency='usd', + customer=customer.stripe_customer_id, + description="Upgrade to Zulip Standard, ${} x {}".format(price_per_license/100, licenses), + receipt_email=user.delivery_email, + statement_descriptor='Zulip Standard') + # Not setting a period start and end, but maybe we should? Unclear what will make things + # most similar to the renewal case from an accounting perspective. + description = "Payment (Card ending in {})".format(cast(stripe.Card, stripe_charge.source).last4) + stripe.InvoiceItem.create( + amount=price_per_license * licenses * -1, + currency='usd', + customer=customer.stripe_customer_id, + description=description, + discountable=False) # TODO: The correctness of this relies on user creation, deactivation, etc being # in a transaction.atomic() with the relevant RealmAuditLog entries @@ -331,6 +353,8 @@ def process_initial_upgrade(user: UserProfile, licenses: int, automanage_license 'billing_cycle_anchor': billing_cycle_anchor, 'billing_schedule': billing_schedule, 'tier': CustomerPlan.STANDARD} + if free_trial: + plan_params['status'] = CustomerPlan.FREE_TRIAL plan = CustomerPlan.objects.create( customer=customer, next_invoice_date=next_invoice_date, @@ -347,29 +371,32 @@ def process_initial_upgrade(user: UserProfile, licenses: int, automanage_license realm=realm, acting_user=user, event_time=billing_cycle_anchor, event_type=RealmAuditLog.CUSTOMER_PLAN_CREATED, extra_data=ujson.dumps(plan_params)) - stripe.InvoiceItem.create( - currency='usd', - customer=customer.stripe_customer_id, - description='Zulip Standard', - discountable=False, - period = {'start': datetime_to_timestamp(billing_cycle_anchor), - 'end': datetime_to_timestamp(period_end)}, - quantity=billed_licenses, - unit_amount=price_per_license) - if 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=customer.stripe_customer_id, - days_until_due=days_until_due, - statement_descriptor='Zulip Standard') - stripe.Invoice.finalize_invoice(stripe_invoice) + if not free_trial: + stripe.InvoiceItem.create( + currency='usd', + customer=customer.stripe_customer_id, + description='Zulip Standard', + discountable=False, + period = {'start': datetime_to_timestamp(billing_cycle_anchor), + 'end': datetime_to_timestamp(period_end)}, + quantity=billed_licenses, + unit_amount=price_per_license) + + if 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=customer.stripe_customer_id, + days_until_due=days_until_due, + statement_descriptor='Zulip Standard') + stripe.Invoice.finalize_invoice(stripe_invoice) from zerver.lib.actions import do_change_plan_type do_change_plan_type(realm, Realm.STANDARD) @@ -505,7 +532,8 @@ def estimate_annual_recurring_revenue_by_realm() -> Dict[str, int]: # nocoverag return annual_revenue # During realm deactivation we instantly downgrade the plan to Limited. -# Extra users added in the final month are not charged. +# Extra users added in the final month are not charged. Also used +# for the cancelation of Free Trial. def downgrade_now(realm: Realm) -> None: plan = get_current_plan_by_realm(realm) if plan is None: diff --git a/corporate/models.py b/corporate/models.py index c517121f75..b512f7c4ff 100644 --- a/corporate/models.py +++ b/corporate/models.py @@ -52,6 +52,7 @@ class CustomerPlan(models.Model): ACTIVE = 1 DOWNGRADE_AT_END_OF_CYCLE = 2 + FREE_TRIAL = 3 # "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/free_trial_upgrade_by_card--Charge.list.1.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Charge.list.1.json new file mode 100644 index 0000000000000000000000000000000000000000..75910611cf34950c38f6deb72141b57c3f9be4ba GIT binary patch literal 82 zcmb>CQczGzNi0cJvQmhS)dBG`5{u(=^NUh}B58>^#i?My{G_bZ Yv?vE8pkHRFpPZ3cl%86w#8t}$0I0AQ%m4rY literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Customer.create.1.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Customer.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..48d96fefd7fd7b9241f446a9d5033258f66745b3 GIT binary patch literal 2000 zcmc&#O=}x55WVMDEIPFi8r#XKrv}nPp=Wmu0g}+}accw8}c#)2P-#e#2W|>Cg^aEHxu^pZ0MT zG}f@by)rYO}A5;M4XJSYl7b^L!<%9!blgiazH%XedFBJ*sepyrl$@v znQ&OTx?@JG!Ys3^2Bk`EGbv7dR@&B9GY(3?ezz$k^~hN_-lBG^>x>dk&*h@>U{3XJ zyn$Hd2C4+=^k}No4d{E?&?%f7>TGznr&Gnnq{)NjL{N?|KiH0npVRYPbzDCVVHuTY zZGW{%O?UHz6h^N17Fe?^yJG`U@avU6ez4h~o~)KmrF`*wbFl7Y8+>kX>3l*N#W$cx zpNrY~bfre2KP@xNMv!Ci?TSG8U!#cm%}}H`=H3%_RXN^>51PwIM`yN67m=KOaQ~X`p%}Y zZIMG(PL7_)^NYECWJ5e#&Cdp(f|%&rfd!t86N%>!29)~c)`p)M)PDex|B3zv;vS+c z(5%)H*3fqBQ=W{8eTU+f4Y~OA=rR8R`iNx@wDonE%#7e9jL8*5vN%>rNjP~r`32b# BFlGP% literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Customer.retrieve.1.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Customer.retrieve.1.json new file mode 100644 index 0000000000000000000000000000000000000000..2485749ffd1a8eb76297752c838ba36c780ae713 GIT binary patch literal 2635 zcmc(hOHbS|5XbL)iYTX6LWNy8aVio$RO(Ab^gvZvUVGBGICg3~t6dP^y)%;s$!-== zsZu%cnEBiD@XT*{ILfkID&-rO#7bJ}6yz7#=`q{24iF>$?HX&@v@1~oWCD$n#p&Xd zzZNW1O$gxBc6?YvAsd@S^iAL}4;Y2ww{e6riS+pwsZ3#1P8W^clw@GVjWZZEyG|H9 z?&_>@u%LTxoQ9C460!*LcvdD<3F?l-wZuwyBGOIEQ3a!9B88rJ-J6;&JfpAO8__o( zauuYbHs;?I4wmagLK!Pvy)D+nZ3O%$I#fBks*;r;EqD{TK_P_5N`AiZ_6GD=t( z)A_Uj*wmu(E|q9L>smp!El7&CJU#2Jg>gS2)WNt!X-rZWB~dg^qmm>zDB7exJHw?a z8T2&$c7o{pP)MI=e!e_k_U9`|Qp+Uq=7+~FQ!I-fB)`;>1|x)C>{q`)ah?>gwCyx0+$hFD?pkwD@*s*&Cc!PZpdhB@__*&@!__6cl0A%?b z1lcj(xDJGDKLkT|j<)6ri0pY5illht1>nU&e3UZc2S%7c0L@SLTN4N7C!eGhJrj9< zQQHIt;>mh((sBwyq~E5W3yYI}BL`5#W;I}VW>Nn^iu_N|-=sLnXh<~4jereQHGc#- t0uytG?1q8dak^*Be@MO0Ws#2Nzqf5so3o#5#4=nu?+<9+}D literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Customer.retrieve.2.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Customer.retrieve.2.json new file mode 100644 index 0000000000000000000000000000000000000000..2485749ffd1a8eb76297752c838ba36c780ae713 GIT binary patch literal 2635 zcmc(hOHbS|5XbL)iYTX6LWNy8aVio$RO(Ab^gvZvUVGBGICg3~t6dP^y)%;s$!-== zsZu%cnEBiD@XT*{ILfkID&-rO#7bJ}6yz7#=`q{24iF>$?HX&@v@1~oWCD$n#p&Xd zzZNW1O$gxBc6?YvAsd@S^iAL}4;Y2ww{e6riS+pwsZ3#1P8W^clw@GVjWZZEyG|H9 z?&_>@u%LTxoQ9C460!*LcvdD<3F?l-wZuwyBGOIEQ3a!9B88rJ-J6;&JfpAO8__o( zauuYbHs;?I4wmagLK!Pvy)D+nZ3O%$I#fBks*;r;EqD{TK_P_5N`AiZ_6GD=t( z)A_Uj*wmu(E|q9L>smp!El7&CJU#2Jg>gS2)WNt!X-rZWB~dg^qmm>zDB7exJHw?a z8T2&$c7o{pP)MI=e!e_k_U9`|Qp+Uq=7+~FQ!I-fB)`;>1|x)C>{q`)ah?>gwCyx0+$hFD?pkwD@*s*&Cc!PZpdhB@__*&@!__6cl0A%?b z1lcj(xDJGDKLkT|j<)6ri0pY5illht1>nU&e3UZc2S%7c0L@SLTN4N7C!eGhJrj9< zQQHIt;>mh((sBwyq~E5W3yYI}BL`5#W;I}VW>Nn^iu_N|-=sLnXh<~4jereQHGc#- t0uytG?1q8dak^*Be@MO0Ws#2Nzqf5so3o#5#4=nu?+<9+}D literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.create.1.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..dcf2a013fcf0bfd255d595591389dab83f2cc907 GIT binary patch literal 2528 zcma)8S#K0M5Pr|EX!#k)bcRFl1R}d=CD@ffNDByAHEj>=#cg+dOcJ8}_pP$s?w$ig z9ww8zkMAqz?yPATbe)YR@bu|!*^7pK{Jy&lJaapdzo!xupGn= zOh=Uk7qfh$w9ctn*Ms|pdC+pev>j-Do0}>|+)41(pgSADM9^8A{ZLv9>#WwYOO+2Y z^p;BbMfeWmhE#96YH4Dt)kP}0i_S^Yr42+cO1kK=K)zK{3%{2&DWpruqUV4A^Zwnd zw|{;9;}Qe5OK0hD6v9b=(x1~lG)3uQ&@#OI5w#j0b#^$2Q9v-aT?*i1Zw*cPSmS#& zj(Oh4$^bW9-q1=ZxuFTMGfmar$$;JRg$X^$|piB`$Cv=fxb(bGy)ss8_)%7HRIOWkk(Trz4f?wuFTG= zPV(qz>{Xkn!j#$HWWq=KC_A0JN3mUcBdk+TcgR~LHI@me{*wM`nk-G4O01c#BuAf9 z_$w+Q_eS)YAkusYU<5ePJZuoR^13i&aw!9Zji!NBZWeXvR{gr$$+%2R;RV20O#Pml zRy#RZaVkAR2g;1@?g?BalimD>RHhs>P@WPl)T4AN@p~R94h}JZ9_{++Mf-gH`0gH{t7N;#vm2-XbD!wFtRFhK=67wsY?^Kr9W)yxmWjm9ckOv8(c_n&BI`VJ1*=F4 zz)c^+gdG8d8F@3G9>~uQ6;T;V)i4u>O+L!yAU~YX@!V4>GoPCBO4j|d>Aoow@XTS3 zX@@N1j~Br0XD+&mw*xKSo_aX(2@)!Bg9pmbFKvFNM<;*F&dE;V(d4z^T4btV|0w3r zs6YbK<(}?|P0!c)!ofBJfVhN09vm32h!$i}F5Xrd(CP=?S6Ge=S2BS6 zj^b#hhZ--^kr6H7NQiJ3%YYR!Kj9d0FF}-WG5e0-8yhE#0R@bcH}%q(w*wgshXX!+7!7Q(rVM}P+jB`$ z5Pe7!pR2#~eaCkvRmGrbY&3zVPj|!quGok7?9tq0pp$fdDR)(IFAiM_6_L#A`h1zg z@@WrB;PGO9=41!TDD!pCzwfnbASi42R!R;j9z9IdI!A{fyT0@|<;`#%e8_ z)OjaEYpIoAq;D{9Nd4A}dXfHCsp%#^-mhx9poqkn66zK;C*Wip?qxd zt?K)X_pvd+HJ8^kQYvmJM}dKEj7^h+N|*lpC0T^rLezoxaMMX->xlN4eDLr#x-|JC z3ng`kKVdPZ^^H?K4Ujs?T?plq80PmvigSUn^D}7#UXX7<7p{#Jx7LOaS&eu{t|jPXTE30URZ)SL^e|#ro;ldaO1t+#{#ZYhXxer*{?!oXL8K z;8g?@RG<}dHdSOkZsrp_NJx^%te8YmY8|U(Fx>P6A<{K*`4E$6)D?=Q3{>;_#pVK@ zzi1cFyN_3$`QD#*x3`y9H7obNPydt35uO%2@2oqR5u4A!hr%lsYyh1fE2_enbu{i$e3eku+~Cgg)8m`p?cT}X z@^iA0xJUVT;jzio!Tz4isnLM?rDZ-W8Jk|TPxJ^??*kYCt~`b08;PP!IM`+akd*L{ z2M5NdtwauG32qhcD0Pb~7v_-RNIG!e@Hm>`?Z$<4#*>|~Vt^MW^M&d3GGj~2w@#OyG2lSxmBLDyZ literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.create.3.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.create.3.json new file mode 100644 index 0000000000000000000000000000000000000000..636a1cb4dd21b6298993daaa21bdda13c76f4872 GIT binary patch literal 2528 zcma)8S#K0M5Pr|EX!#k)bS8jipB60$v0_02A+10wtGex>y}0d;k4Zw5|4x7)Q(>*uTDGb3 zUWU$6DG!8iF>Xlp)~gx^hCi37=r%ehO`A3ly(sCT#{&6QNiF%YJZo)#5Tk%#Zo3q~ht3+B^0CHu zY8djokCg##xV)j2QgTBv${46)ESd~dYWmxMNh9PIq7Hn3+g>7Kr)f{YM+~AvRBYmuMVWamTwo7k>b?WI3d5fgRG6B`!(qC1TrAadpYo;s7 z(dQKYib}}65q%|yRG$MF0Zvp;E5xn5E({r6$^fC!G_Z-AMP0g84|h8nmx(F702qs@ z$GK^>lf4zE(j#=B%;@f(z*REY=6|Fz<)DG`lyIRQq*ICC^FVQMhynDd*PHdri_PWb z^VtEFK7gZu_4>s{y;-Ar%C<~AAb!wlL_;ZOa2COufq6K=s|Y5jK~%qf|V&2m`> z5efep0y6_jvSVtDirax^h}cZPJse6j>I#`rHmZ63w%Nd&x83Tm{@<(KTsMCYzIpfl znw7@y)9*xSoKFXy_tqUOi7gl5LxMgU2OB`=mx|iXU9(IicD}36ONk!8{1#c~nJZXD zS^#eP7$)opAk4^{`Sd`3cBqJnp;Qesv2XHGE(iH3>I-$}o=TaS(dCt#_RGfmrcA&y zhdIU_vW!1o0JopH=q9`!Xz}(;hZCP5p#nE}p#1#O=4X0v@=8b9L@Mp<3&0$q9q&&5$<9caDvQlI7Zw{5G7p9{|zCrmP;g_;j-F;-JWtxa6d&^ Yb}gIE+8-WAN%+Nu=IUoSp534Q0;gf-5&!@I literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.finalize_invoice.1.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.finalize_invoice.1.json new file mode 100644 index 0000000000000000000000000000000000000000..857c0a8b08a143b3a372d25594a0924c76da7e6c GIT binary patch literal 2741 zcmcImS#K0M5Pr|EX!&_zrZXIZCjwc}YS{&WKpa|G?zV?ExNXPBBq7Rwr^*}Q>#oV;ioAPE{+O#Y>jlcs#jYym#h_2FJfgZjUBf3k zX1QgOgJMtSM9-v_`pd$@S|wW+q|tOM1Z61#*yyNKjv0dJZK-pLnxz8sY?<;R+s0rh z<2$BXtSq>g<&9J-rzUO3+!x3LmOZ3xnNruesbWN(VBTnOCq2`VsjSU@ARP;9jZ&gb zmG>fa221%x_!i=ZRByem5wZ39G!@-O=Y($41fu69UGz{O-AbXj-^-d5!lh)<(|=#R z`RDBAU!VRwhk*6mSvY`#JK>M|Q=EswC>`sS2v5F8CGFkT^otl}38uD70n~OzBg%&w z-$`pT-iOM-uBf=ekz#UEnae!D!u7XRS2r=&q)|_(i1z8y!K%FWRnnRnkVXACg7H^FSX+mQe zo$)YxD)r9DR?z4&@*!CNc(sDY?)m_=2*iI?tX4p|us>gzFlzmQ7`G{Z>euhjnkF}s zSV*K^1{rOOS*gnMEdtT~Z&j@@Ljn%im_~5}=LT*Opvxfj=k%?rvNUPNdoW!Yl!MPP z+7uNRdz1Zyn_ayPOmpUVb*BQliOWLl!KKWdH45iEo|K|4NXlO$IT#l_B^)jURQ{fu zk~`5Gek?s~JS!O8+#-M_lWD%f1;QK%C`}1xYAc*fXK4ZyfP z07@U2!+`Z>vwm>@A*jb}^TZYw9;}A_F6CHffMc0YO9U?>(?JHja`h}F@v)f?YY`D? zn^#~WZb^2?=s|I95kuJfM7AX;QOgS;beX84^^4{aJA2WspY1E)n3O5 zO!{z5$iadTEv}}E$?|K7MGV}}mrBRJ$!|${NA7^WydqOi1<6d^^0<%lCBs>kI>2>C z%oKaO}K8X~1lJiH1XC(sR6jXL6@#8*z7uu|m&fG&rWcM7p59T*9r? zOv&%?R(^b literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.finalize_invoice.2.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.finalize_invoice.2.json new file mode 100644 index 0000000000000000000000000000000000000000..8575f0b461b43e2b5de8dca4aa6e19ddbb8a9a3b GIT binary patch literal 2752 zcmcIm+m0JI41M3P7|c@(B(cZ#cC&eEkZprrlA=vp6bTA}9?2Os8cC~*vlr~YhmX_>Fp0!n6lYDEF40?o$x2mGn$5?EFJVpgzxX7lJ-e!`hy&00j6u02AJ)P zCX|mozLVBwypNp$ZkV{Cky3F@ISLGPV{DoNWV-abf0Kuc4MY`~h1*`>wvK3z$%hDU zqDzxM@=#KT_!AamTHiWpX@Jy8?t)97$gq1aTye(nEZWN85bD&K&>XMH)}YG9W%I_E zkR~*?(HW1kXHsvCYz2!hBOikG&t@~c*b^VH7eV;XO4JPL7WU_h6DF@eGUt7w&&7rK zeI@f{5(jIk50ebhixO74vV228bpKmdGpdk)LpG*S+@sW>4k5aLrQfA*Rh6yDGwQ*h zFl5COr$|#=TmQ45~t<5!#FDo1Z~fi$~RB-Yl!f)hlqrw{KH>L!z<^uvRqt zQD%y}pew(O?hsrsmMCTfvi!PZN^V7O_^I;90w}oMKM<&;l3D+UszfyiWR?oO4uyVFXor$Pgmz-wcWxNNq|wq&X;zqGswHl zu|ou}Bj_LlDc@?USo^r%o!~)2VshS!iOZ$dG06wVttAMN3Wh2ciZw6S zm+<0cH-FLpbKUEY_M*SLySi@ba_@)qKZz5<(}8EbaR)Q1cjw?k*{eF304l$?glN8X zUG!kKP4p&-@W}JeF4ip52fIl4M@A$AAJS6nxKN*AATcv7Fg7>nagC(IuR|&N%5YvNwAAcmroG=gmWXhQ%=JgH U?u_~15lh-^oao`r&mPV`0X6j`^8f$< literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.finalize_invoice.3.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.finalize_invoice.3.json new file mode 100644 index 0000000000000000000000000000000000000000..1a4b0cb4eab2e148d52a2d84f5b5c682390263f7 GIT binary patch literal 2741 zcmcImO>Y}F5WV|X2zzcK$+BBGrxuA}q)A(#L4zVeL2!4eEE_IK^Fy{{_`mlJ$=#J! zN{~x+u%Y>$H}i&iJXmip7o!&)U99;DH1D+J>y0&H|tD#r{#^tRMFMa@z{dA3}6 zk!@ozjJG?k8>%e0nB}!pDyOcRj=N9D1Ir$18?MwtZmJkDC%88n?p4oq}AUM;Z$1fInd|hV_k;mIg?b$cJG4i{%oHJ@)~%2*Q6^td^i$IGisgOj>^=rftffO}vaR z-{)o$3oEJTK}OqRR;se?mVoH~x2l$uApr+ArcpfN+~5`gU5?Zr)3?PUOOs~22h)`y zIebphrl?wRFxd~Z*^769>z2EA@oWLQiMvAV!KKWd*NWymo|K|4NXl;`IT#l_B@Pz> zD!l)?%Ex+t zShI*k+q?o3aZ9peMh}VyOBiDB6WQjVL@jSY=rU2o);IMPzrN{KFZ;i5dwo~`YJL6s z?OjzGe@g$;B?3Mj_pCQ=Hl%8Q5I&?GhjB20tNg%H+qvtikbk*LT&8>ALF%t=)-rPi zRipu6(#LC39W6+u#rVpZDn8@*Va$J9CByY ziSM#=q7irm*oefiftd=Y&O1s2v*{%o4vk6A@!_4xosw;&-K~rjdSAwZ)Skq~5Php=QjF4R{TNX$$NOj|Sk t!~^Xh7?M@QLQJg5lb^)2`7_5vhEs&D?nK=g^VtJW+HOpB-u>Cr*?$7}BL)Bf literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.list.1.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.list.1.json new file mode 100644 index 0000000000000000000000000000000000000000..e39960ab7254f5b071e40ef45d6574c984929f3a GIT binary patch literal 83 zcmb>CQczGzNi0cJvQmhS)dBG`5{u(=^NUh}B58>^#i?My{G_bZ Zv?vE8pkHRFpP5&dpP8Imti)Bz1pvYK7f%2H literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.list.2.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.list.2.json new file mode 100644 index 0000000000000000000000000000000000000000..e39960ab7254f5b071e40ef45d6574c984929f3a GIT binary patch literal 83 zcmb>CQczGzNi0cJvQmhS)dBG`5{u(=^NUh}B58>^#i?My{G_bZ Zv?vE8pkHRFpP5&dpP8Imti)Bz1pvYK7f%2H literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.list.3.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.list.3.json new file mode 100644 index 0000000000000000000000000000000000000000..67b144d5887b5df85d4510a9b7f6f0f13b666d46 GIT binary patch literal 3211 zcmcIm-;dNb5PrYEBJy)#H@83qPh5KiRi!Ng6;V)SIf<7T96RS9dv~1ffA5U##7Q<= zc)`|hkLNW&Z>&x()myD09$7gkwaYp)y)Uvmo%zKjdtr5`p)WFDX-R zG;ld`cCFi2o+&3BDsZ_dgIHfphrALUr6R&;E*2^Ix{?gW+aCK4yb6R~YoJ%+3PdC! zLelbO^^{$_Y_~sm@2@&_{rBbh!M}L5Ba`ObkH1HCgnqWn(oUO|L6hkqts6N7o1At` z#>WM=Cl84v@!1@LmY^P45S5dN-9&z-%EsXVa@OHTR)I$pzsU;E;tK%_8+0fusPbJE z@5gjEJ^-JIOyx7D?vKYN>9I4z3M|HJMJNZi3r?*UaT-76Y3$xKpWnZ5zfEEV0?>r^ z<1!;b=m_=|4-ozFQiG>NBOFz5JJ|f9%BSuP|88HyYYvwg9XmBecckB`>j}yQrNLWv z5J4TDB6AJQT|j@2{2;pLYPn9GVaf`mDEefnAbX*h6kp-aV?hdzx&sjHnE3(@UJWlZ z(ADTZJr6kISXT`|zGg^)dyYW*A1Z(f5lDh4oIjm``mi`Hf}k3}#OsKlxe1xJ5+o4t nl*o{4UbR|(u`mZ@5};$*sc;xngrEhhDzL`?HGY$``?G%m)sAVs literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.list.4.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.list.4.json new file mode 100644 index 0000000000000000000000000000000000000000..67b144d5887b5df85d4510a9b7f6f0f13b666d46 GIT binary patch literal 3211 zcmcIm-;dNb5PrYEBJy)#H@83qPh5KiRi!Ng6;V)SIf<7T96RS9dv~1ffA5U##7Q<= zc)`|hkLNW&Z>&x()myD09$7gkwaYp)y)Uvmo%zKjdtr5`p)WFDX-R zG;ld`cCFi2o+&3BDsZ_dgIHfphrALUr6R&;E*2^Ix{?gW+aCK4yb6R~YoJ%+3PdC! zLelbO^^{$_Y_~sm@2@&_{rBbh!M}L5Ba`ObkH1HCgnqWn(oUO|L6hkqts6N7o1At` z#>WM=Cl84v@!1@LmY^P45S5dN-9&z-%EsXVa@OHTR)I$pzsU;E;tK%_8+0fusPbJE z@5gjEJ^-JIOyx7D?vKYN>9I4z3M|HJMJNZi3r?*UaT-76Y3$xKpWnZ5zfEEV0?>r^ z<1!;b=m_=|4-ozFQiG>NBOFz5JJ|f9%BSuP|88HyYYvwg9XmBecckB`>j}yQrNLWv z5J4TDB6AJQT|j@2{2;pLYPn9GVaf`mDEefnAbX*h6kp-aV?hdzx&sjHnE3(@UJWlZ z(ADTZJr6kISXT`|zGg^)dyYW*A1Z(f5lDh4oIjm``mi`Hf}k3}#OsKlxe1xJ5+o4t nl*o{4UbR|(u`mZ@5};$*sc;xngrEhhDzL`?HGY$``?G%m)sAVs literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.list.5.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.list.5.json new file mode 100644 index 0000000000000000000000000000000000000000..58547c136a21aca9363ceda51e0ee64b74e62a7d GIT binary patch literal 6348 zcmeHL-EZ4A5P$DqA@FGf+S;8)PI|EU?9K0ArFUS z#R`tJBfd$(kq%SgMdTH-EYkg1JYG{uCUPK6htCZ!m{3A}+&Wj5Qe?~tt>}t#%Df!l zQk9fZigUaugYA9!HHD+r6_b|7GnLbn`26_b&pJ`a?h!I_qlXZ}_NcWaI2U85=k9nRk)ou*U&nYWk8Dr30H zeAFtNchpxF45ur>Wp1~u@I-FB^2*Ymet!Mx@Z}Hhzdr);=}-ta;I7U7MrE98*P<-z_>40+c1HObrUV+JP*%rP=n=RK zG70aL4}Z4QoUd3V9WB{;!J!)3==2mdTvk`cXU`}V6&Q#a`m3kHW=514e6%iFmJ92P zZ+LGgCki!-{s@7G@U#|lm4hqjlF5+Ck+jm<`RMk{TWbq>M1|UDk#Sm?I%3Y1_UU8- z7k_LW*vBz$lfX5BQg{9QfDA#Z-FjT-CVtQM`LBB-UeMP*5o#!?IWg<72J6avg-%-E znspP@&^i;U-iNC}#UWZEVn)oY|B)7xx`-EOy9zUqbKc8^!#o$57fdFAdR$qaeDxz! z$?Rb=o2K*RQSyqJ=HJ(?*^RJ}KWr&6S9)+4SV9EE^$}t@}p90c%k!M zra)z}(1|Xung9f?die>^5%pm}s8>EB(ps6$(>btJvF>jw#OgZ5YmG}Bq5L+#~kslF` z=uu}@A3uWbad2sf=OwxwO3l~!nExszP5@UzLP(lEUmmb$&)3su#k-S2otFE>#l_J{ zN;eFM>wDlw0#by3xFP~pkdfJWW`&T zhJP`y;TebiMq5vHv%9XoT^$R|1)#yk+aRO%)5&`rB$79dshpY4c!T^E)(6ogSJBh; zbLtFJ)*~wNM5G`q0T?STpbavQ1Y6#Lye^pe3=EZOhBbiIn!S4;V8pPF5}^3AuMjAFc+@okA5n-mGz`2za2V*F*mX>q=UWX zj&2NxaYp^)Xtt@2U(WCN#*p5PjUi_L4Q>qI#c0f5=bzpkyB85S?>7dhPh3C#2OC4b kiTuNzVJPHgwg!Ms|Mpe#@+~!2UidN0#)q3+W-In literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.list.6.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Invoice.list.6.json new file mode 100644 index 0000000000000000000000000000000000000000..fdee84eb0d8a7bcbc4377c9c3b1bfc331b7cfd89 GIT binary patch literal 9474 zcmeHMS##Sq5Pr|E;P9!PNg`3SqAE{yeNAGgN78!IWI7y>gd!v)NE4uZjsJUh0gxai zGID*@nMe;BiMv=V_Wp>=CywLBL=yL<^AUbK&ZYW>rzDE9lFLAS7w5p>{`Jm%x=iLI zrAXVQ3&$*JFr`XFTq27%9u0>>D?rn7!eSt9eSGgNQQRuSwW)j7PO2gZfZR(+VL zEDFja<+)PCjqOVTiU%`BlUS^1p@_6lo0lT^^|!ZgwvS$Y*xLo+-m(x{fVtv=ie5PZ z`Y}OibCM=h{(Mm;EPozlX@@KmOl`9mg@SyZar8o<#Daz&@ad3&apk9%6^yeWkJ;nzrkv> z%i|duX?}0o4RD9rn2`0#T~%dVu9#RgZQa!W+`1;UtDd3urKTarv=t2>Fk_r|lJJ-m zvGdGJFru|mQGTKPZh`_Vmi;| zRb_0mYZiWnN{IWR@dLH_5=%}Crb?uBi&+f84=X)?>Q6?~t*vo;A$U(nQlN0pe=+i= zKCqakrUfi3lS2{#cQaqiPI-|*F6*4_vi3*$EU86A?+^9fVV|cQ7CBr0-8sej2{cMfmFI)NL5&s{UP9 zN3iFdh#<|14oAC9M+#Zz6fBaNB#9m_&^*>Ut|>l)g{mc3j|3Ez-iRIO{4|=?&V$J5 z4nMau@LW#MY=tL!LO{i;I%F#-FVj%>N7Eah0Z*e$*)tpK*T;6vu+xeac&OV7;}W_H zdM#(#8wb`KbbfevyjLDHWT^xQOti16sS_lqgS~?TihkYHV3dfN3^;TLVJ9kk>b#)W z<}GMMp_v7>rJB*L)32{Rfx1v>u*+6OFjJ#Qy9Qb-Ab*MZ;buAI%g+N@5H1*|DDv1+ z;m#N*3A=!vM^q_Tu?~c2N{TPQQ1WUrgSx8Oo96*WbZaL8k}s~dz%?sD`jskx6+$Hm zMqzt&hSZ0LO(k5c2H^2XM^KxDnbsjhC178XaZhPD&$2H9{D7GR(XnhPbr_Rxm1XZ9 zHH^K(DIE;tW|7)BHN#}>1teH~&L zHCXQ-F=*}X5-~I@SCzFrP~MQMR%y`Vb_|^yev`z3_au}H>iqDUaA)+?84W!j0K^+o zRR4agtyKxTJ&Ld%Ffz>`qz8E%OM6ko%F@0u&PeVY<_ygaVz)pM6-20tq?Qd$zaFgo z@nkq1|MUjmotk#PZ4w zh%k+z14LMiwgC}fC4-pEjl$;I!TrCl9lYO0OAyhn zT~*%xCm7Lw<#w;v4%XU6)5_-SuN_vFeglBPAHEn*9_HF%>GtE1UOQ~t8TIeo(W2Zx z@*g>3@E!+Zu+e{qh~XFO4gYQY`Tf2;w1snn7_hy6C)W<`(fMYWq1}nhydC*N*%BK7 literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--InvoiceItem.create.1.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--InvoiceItem.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..f99447209e69e956df342a2e20427de6b7f67010 GIT binary patch literal 517 zcmZ9JOHacv5QOjj6_L*%HBDO{HzYV9@sQwFgrcpzNQ)dh_)$Pr|2w;G2ra&4yz}jN zvYk|wg3f1`)MZuArhHCB(>Vlm`kPgiqfvtxlkYHysB>%|9-r>7?rv8%*KDizGX@e0 zN=W4tr@^QL^@+R_^D0~FtLFsHKrq#5#k=qm?0}%CB@wKxpx(enEEFM@X){adbw3qmI!b+eq+ z7tMUW5I@T?K`2!=&2+gq;|tdJ@ZN$ve$3PdA7E(Y?~NZBT%sh!*9EF#ULTe>`V)M$ i48X{vFRvwkc9h!jcb;=I|K--8rjDEDH_E-5>?S|J9+7nb literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--InvoiceItem.create.2.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--InvoiceItem.create.2.json new file mode 100644 index 0000000000000000000000000000000000000000..04af1e14a49f98c3d06665836223e7d00749d2cb GIT binary patch literal 536 zcmZ8eO-lnY5WV+Tggq%#NcO{8Zv{OlR>7kpOPY)@l1}HG=vUADC;+i#LQO*fb$$PNsJ7Yx(x|qNaT?d{(l|uXY^t@g@+`Zi1aL&cZ(K14Q z7a1APpwtkQ$3CL7BwlHabeD!1RA2+JvwLY-IcFso6}w<-=?~TUWQ;-4lyYOJ!Mij8 z25;14iFi8Bp*`p2s7JeXxmV`eWXzy6xP{K?Q@akL%%6~%xZ4jjxA_1?=@gR(M(9S< z9y~hgzssTJ*uYXDs>@O=tGaGTpWzUtCj(J63%+cw>L%l(d2b{|a-6C6&dVvM-#>mP zX(L8jM?K6G(&lfXM|qTAZPr7`%-6Spnk>flxM5K)tN(DTL18CN))e$!%=WV%GUJsZ literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--InvoiceItem.create.3.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--InvoiceItem.create.3.json new file mode 100644 index 0000000000000000000000000000000000000000..47ee85f59a310277221116d1032d096e43b94364 GIT binary patch literal 517 zcmZ8e$xg#C5WV*+qMSiWnidE*Bsd_kNN_7c(bOKKL5>}~6j0TFXT}Mo#Wz2{W#)N1 zD~b|&muxIAi+VY)s_H~AmApr*z8Ni3(B%Y%=z8=5sua41$EW+NyW7>xHOuOeqamVL zF*2U4)EJbfA(FEqUL`|A@f^Ww@VYoHI14|)OcoTBOayBqR&QVeW*5>TCOQuO4XUbU z)RWyfQX;j9IG2rp4iS8=vN@aAZNm?YkLH5`S$;p$;GKu5k-r^364*#lim%%|R7mT?^7i@& kUmY7@VCu_jrY1|V+kfW;H}fCvG%D)3Y2H!Z%h_)B1Juiri2wiq literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Token.create.1.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Token.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..4edd6803f06eb68d063d9ba37efe61abdf0fa23d GIT binary patch literal 826 zcmaJe?5488L!s+`f(7EZe%ZHGzw(hxiBQkleEJ=zo{E*c>I`{F>^Rw2?$YCk`} z7kfWS5~aZzb(`$NJ^X<&4!til@is)&D`+Z7tMMS+c9va4EG#jaWX{0ohq#(7X2P?u z23^7xCLUpY5mx95^-92z*n`RNv0XrD{?Ab6Iw2gBTH}M7fHoU z7_k&&X?B#H39mXD3*6UzshT#}v_=G%P<44qP-h^=c+Esz`vv?R5=82 z>bx%2(8+9)h&~6tW@)Rzp!=J_CE|C|MBpYGq6#=9^p5c6Up6>w?+*O)0ONC}xL6^J1~ho{11*6neG zl+ZmFe_qHgGLjl5Z$*b-^x8H$nr9#6+0vN8Lh5}{F?&Fq&ie#m!l>gbk7sZM>vf6c z;tZ8bEt9<5O+gqd>R@#a>R7>;O4Djzv-EY+mY(zdje}DXIE@~mq`@K8JpJ(xsFKb! zB^Kn_gcOvmh(IPa-lYkT0ffI5i o5_Y`c$g4PQ=o`8_Yq=7QuJh}4U=m?sgn%6?Ieq(OOE<5Z|4tlX{{R30 literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Customer.retrieve.1.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Customer.retrieve.1.json new file mode 100644 index 0000000000000000000000000000000000000000..859f8d187d410ab402e6ee0722d60e874ffc8cfd GIT binary patch literal 1199 zcmd5*%Wm5+5WM><44qP-h^=c+Esz`vv?R5=82 z>bx%2(8+9)h&~6tW@)Rzp!=J_CE|C|MBpYGq6#=9^p5c6Up6>w?+*O)0ONC}xL6^J1~ho{11*6neG zl+ZmFe_qHgGLjl5Z$*b-^x8H$nr9#6+0vN8Lh5}{F?&Fq&ie#m!l>gbk7sZM>vf6c z;tZ8bEt9<5O+gqd>R@#a>R7>;O4Djzv-EY+mY(zdje}DXIE@~mq`@K8JpJ(xsFKb! zB^Kn_gcOvmh(IPa-lYkT0ffI5i o5_Y`c$g4PQ=o`8_Yq=7QuJh}4U=m?sgn%6?Ieq(OOE<5Z|4tlX{{R30 literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Customer.retrieve.2.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Customer.retrieve.2.json new file mode 100644 index 0000000000000000000000000000000000000000..859f8d187d410ab402e6ee0722d60e874ffc8cfd GIT binary patch literal 1199 zcmd5*%Wm5+5WM><44qP-h^=c+Esz`vv?R5=82 z>bx%2(8+9)h&~6tW@)Rzp!=J_CE|C|MBpYGq6#=9^p5c6Up6>w?+*O)0ONC}xL6^J1~ho{11*6neG zl+ZmFe_qHgGLjl5Z$*b-^x8H$nr9#6+0vN8Lh5}{F?&Fq&ie#m!l>gbk7sZM>vf6c z;tZ8bEt9<5O+gqd>R@#a>R7>;O4Djzv-EY+mY(zdje}DXIE@~mq`@K8JpJ(xsFKb! zB^Kn_gcOvmh(IPa-lYkT0ffI5i o5_Y`c$g4PQ=o`8_Yq=7QuJh}4U=m?sgn%6?Ieq(OOE<5Z|4tlX{{R30 literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Invoice.create.1.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Invoice.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..d0c4723502ac7fa3f03d06c310bafc5978210196 GIT binary patch literal 2513 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 literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Invoice.finalize_invoice.2.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Invoice.finalize_invoice.2.json new file mode 100644 index 0000000000000000000000000000000000000000..28c295334fe23c411eb09531b08fe7ebecd6a454 GIT binary patch literal 2720 zcmcIm$!;4t5WV{=8az*;&2dIK%}f#)0cHVW3=%j92&ze?>2Na_*^c3VPZi1TmRian zm(jt7YOh!Iih4Y28p7JvCKGA?>mSIghWvX)e%V(UrUlC1h+R|7tAWn0JfV91>-l^> zFOEg-nB<^1ka?k*G}3&TcsQqI%c8V~ZiS#MM*tg>lqxY}6x!7~$EZarFwd5$Ac}2@ zmNLF$rp3ylPes0wN|n?k2*ar~J1fB?J5?q1f(2{9jc8^PQ$^c$%zwdPLqV-oO0>CR zEn;V}gcD&~h!=Bxi}?Zog3l`FYm@iFv>EXfcukW+w3=?EP&^!D%L?H!Nt*)w`=9rJ z{r=~>>$h(p;9}odcxat_5l;G3#9v{Qj%g+0Z{L!V?s;qVBt{+j*lsO=y3U$IAbL&c zq;mz|W4Fleskp}xVse8qDh${$6paQlH~r~z)^M?9Nk!_|gBGx%1Ik12-XhrK^Q4b7 z7}Nv)h{X^#v|c(K99B}Q;4)+afMkr31m)lhvN||`I#&j}18vd`Q+30Ncx!FU6BXHEwou6`A48^X6FtbQ5=#(7j8YJKHFn^Ke3{x*{q68_C|d;wj-!A)xZ-+?3o2 zZTYG6u<@*7^!S7T)=YMD1D6MLAfPlS+*3Q@Wj;qEpa2{!EqE+e>($HU`uzOG=m5$P zna6;O)x~nLUV(bZwx8I+!h==6-?bd)EpV*xX^P-&VkXLnS8lP)hp#$?wAr85BqZ{- z?7+y}>Ud1)Uh&`%L|FWM)u*T=BX5Dxb)yNkE+%bJ|dvo>PlG^!G z{+~|~0MxNSwe^!Jk^M=8n3o)kqm4|J7Zv+1NR!I`*)EeAAD#fL{^4rP3R$3vJOLQ> z@t%;Q4IxI{k0+Cr=W%aj#3a2b&q#ejp21mI7J81|9K4M69Un$Z`im@0gu{%OJ6Z6& z8S%V|A71~_zU)fqvXitfVSkCr-K@G{2Pay%p~D*huPK+28aT9J{E<+@c=1zqUbF)4 zKOK*KCBMNl z!7>96x`$=b%zp)CGW`z@I90{8uN#bTL`OoP0q?`2?XXHe;2r^aUR@||yt@Pl2ZBBs nex)$6woHB$d literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Invoice.list.1.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Invoice.list.1.json new file mode 100644 index 0000000000000000000000000000000000000000..e39960ab7254f5b071e40ef45d6574c984929f3a GIT binary patch literal 83 zcmb>CQczGzNi0cJvQmhS)dBG`5{u(=^NUh}B58>^#i?My{G_bZ Zv?vE8pkHRFpP5&dpP8Imti)Bz1pvYK7f%2H literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Invoice.list.2.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--Invoice.list.2.json new file mode 100644 index 0000000000000000000000000000000000000000..98ed046deeb23b40ea55c3033e19f10e3950e15d GIT binary patch literal 3190 zcmcIm+in{-5PkPoEcl#6TFY_kJf*FJpg?bE(!g*~5Y#T^Wx^$CUgR76@0}sJSFPH- zR1bo*Gn|>7Ip++YPKtum%ro+)_zb^`;wgT^Q&v?vC{N?JeEhN*kQl zE!_z&Yj==68o9-!RdDpT-~asehhKiaes>AM%XtuPp*^+b?r0o}bCVdYVXfr;?PHL_ zysC7&5G#Xc+KyZhHH}t>&FZYK5yqr>EFbsmp7MKy02ZS#)~Hq35qKGN60s?t|L(0d z-?1P)HSD3~z<_=#lSG}D)4@ixELt&vg_&W$CMjHHg~6d?bwg#faFKBEbQ&*k3OvPr zoBx|48?c7US}!!+15G4G9gvs%H6zj7Yx<2bifM*0fq z?WgzvgrL!>x$Y zIE*D92Jpkla&vxKUTn_JUQZ9g^qglFgO{7l`uz2G;F6bE3NSCm0ExhR%$J*UaVsr#Feq^gDv}`~ zY59J8alL!rtbb~+Zd-MCb9u48di>*&jJosO|L)}x8rm>NTWuF6O~#XSz9$tN^4c?* zZWGv_p*tk2&uSmV1edA}){P<(vJd&_aGtfJw95g=L zdz>$LfZq4}3~mv%@KnKKaKq(k6wdN*_O-m?u*YcMsWGk-^NwvMC>KNq7i_10I^II2 z@;7t={R1+CXv5X~IrWw)Cy<@!GpGdF3B{y%gmuTF3LJC?z}YhU6&!-gGyv`DE(M-BfakyPAGIOU0}7w+4+5lzhvOpf5rv6Y2|-g6a%>?;q~0-kA$NS+ hX#Lf}9FRkRj(Mlz%b*rS67Ip++YPKtum%ro+)_zb^`;wgT^Q&v?vC{N?JeEhN*kQl zE!_z&Yj==68o9-!RdDpT-~asehhKiaes>AM%XtuPp*^+b?r0o}bCVdYVXfr;?PHL_ zysC7&5G#Xc+KyZhHH}t>&FZYK5yqr>EFbsmp7MKy02ZS#)~Hq35qKGN60s?t|L(0d z-?1P)HSD3~z<_=#lSG}D)4@ixELt&vg_&W$CMjHHg~6d?bwg#faFKBEbQ&*k3OvPr zoBx|48?c7US}!!+15G4G9gvs%H6zj7Yx<2bifM*0fq z?WgzvgrL!>x$Y zIE*D92Jpkla&vxKUTn_JUQZ9g^qglFgO{7l`uz2G;F6bE3NSCm0ExhR%$J*UaVsr#Feq^gDv}`~ zY59J8alL!rtbb~+Zd-MCb9u48di>*&jJosO|L)}x8rm>NTWuF6O~#XSz9$tN^4c?* zZWGv_p*tk2&uSmV1edA}){P<(vJd&_aGtfJw95g=L zdz>$LfZq4}3~mv%@KnKKaKq(k6wdN*_O-m?u*YcMsWGk-^NwvMC>KNq7i_10I^II2 z@;7t={R1+CXv5X~IrWw)Cy<@!GpGdF3B{y%gmuTF3LJC?z}YhU6&!-gGyv`DE(M-BfakyPAGIOU0}7w+4+5lzhvOpf5rv6Y2|-g6a%>?;q~0-kA$NS+ hX#Lf}9FRkRj(Mlz%b*rS67Ip++YPKtum%ro+)_zb^`;wgT^Q&v?vC{N?JeEhN*kQl zE!_z&Yj==68o9-!RdDpT-~asehhKiaes>AM%XtuPp*^+b?r0o}bCVdYVXfr;?PHL_ zysC7&5G#Xc+KyZhHH}t>&FZYK5yqr>EFbsmp7MKy02ZS#)~Hq35qKGN60s?t|L(0d z-?1P)HSD3~z<_=#lSG}D)4@ixELt&vg_&W$CMjHHg~6d?bwg#faFKBEbQ&*k3OvPr zoBx|48?c7US}!!+15G4G9gvs%H6zj7Yx<2bifM*0fq z?WgzvgrL!>x$Y zIE*D92Jpkla&vxKUTn_JUQZ9g^qglFgO{7l`uz2G;F6bE3NSCm0ExhR%$J*UaVsr#Feq^gDv}`~ zY59J8alL!rtbb~+Zd-MCb9u48di>*&jJosO|L)}x8rm>NTWuF6O~#XSz9$tN^4c?* zZWGv_p*tk2&uSmV1edA}){P<(vJd&_aGtfJw95g=L zdz>$LfZq4}3~mv%@KnKKaKq(k6wdN*_O-m?u*YcMsWGk-^NwvMC>KNq7i_10I^II2 z@;7t={R1+CXv5X~IrWw)Cy<@!GpGdF3B{y%gmuTF3LJC?z}YhU6&!-gGyv`DE(M-BfakyPAGIOU0}7w+4+5lzhvOpf5rv6Y2|-g6a%>?;q~0-kA$NS+ hX#Lf}9FRkRj(Mlz%b*rS6-#H{DO18Wd z!-{N}{a_?ISDnju&hf*+XhaI;7f45B?jTvOL$da@2pDJ76Hcosw@h$%>U6 zX~+C}6h%5rrB_kZ$g=qMa59-B1%XyEQ2=r4x#1-fN~ntg$*PhfXHIBESDaJkH36Th zrA+#r<7E~7?^_5Ij&@f}TApNTrztD0naa^)XX->-3Mqp-#Bxp2!Uv&1*}4{^k8o-~af-`MYCKo^2}O2EtPqZg=XTm^X>q)~uA= zeRp3=QN7G{IiM>RPd^;dAzH1q+74DjwQEsTAs@ryj@?pzizR^0DAd&n6-ESJ2APBp z%BMeCYQa~mmX22JuH?`GZB}}Q8ZN7A3D>so=T05WJp1QTRP)AJ-)7pfLIak@&<1xJWOY6Wt zj(Ho$N*_aeyMBE@g&@^#J;urmpXudX9-X8Y^xaN`+Emo?cUVJ*(W+Qskk((#yD?g5 zg9%;k)72p1CPE@gCY%iakrspcC>N=_S~HRh-m8Xl?3mH1W2#`L7`+%lTl1R^kThbk zR@hQvUG$(VvczbMr=#5n#Fz^JPYk;Fa#1O+d8vzCu0R*DIB6apn_x$)dU**5i1sk} z)EnPmskjjy8dF-LFcd#XX0yfY)pT)qc+?k!>{-VQD$i!~X||XFOB`ZB!KO16G!bxj z`z2bcjD}tgJv$WbtC}e%93pTwZGkxo+j`l}@_>58icm*=w1^6Zp`;<+RTy|^EI*Zo zB3LVN1t=0rLelJHxi}Xm>&e^l?5b2Ztejt6p1#+l6P?HQ?-m{*plfDnsm(y9NjH<$ zwM_*=PCF*UHi7X;St1dA#@8N9aHv{9m7OI!4h327#ZP!0wQ=BpAR50@zqr|$Q+Z}L~`WG2); zjfLJIWy1JKZ|i)31Mt3`XRt*S!chf-!KUS@qt5Uz<~2O$Fvn;cscv0|&f7Jaz+C_t z9Iy@kX}^W^;V(r2`D^S9qARX8pHpX;vI4skc@j#Hl~7EIdl+}jqrg^lpg2orJ_AFo znq>?iHL!Qz2N*G}g9K>1qA`IoTjlu|{ZD}A+XEc$&kq7k4-dOe;3End&qD?EN!Vip zN_^|>f){ebmusy*TgU_U5LCxzr2NaoPHQ5&KM?*K2g0;LT3GM*2ZBFx((b=M5Xf^M z2-)Swck_=4&YwCEIyY$?x?l4^2;TjF4}=u$xM(&pfBHb!65X|+Ma_e2TBLQ?%D;Ue zG~0gzh(R|D^>%TZ%_j4sSNj724~cjntkPn7alDwE-T!+3FFz2n=ROcp;s1jN!bQqM yb@su`vkz|)X6_FJ=w)o<{eb{+c(wxp&IJE6!Qc-BonDY4{xmRj`jdmlgFgYSp|CFi literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--InvoiceItem.create.1.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--InvoiceItem.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..f4d06581cbf3412ebd72b96c5be4e0d408e12d77 GIT binary patch literal 518 zcmZ8e%TB{U47~R%qMSiWnzppukl=vCLxNioikw{|8EtmMJ_@MnzhfsMwB)wqnelkC zpJiDNtxGoMm)T~$D2n1tFqOPVtG*e{Q_%SchUi-K0;&}1hsUS;tGnCn%{9x)fukWJ z&r3#zuUBde%2OA~SrKoOp)PxlU^RH1ooBoYKf#O^6qF=_#)#D$n1H2z4qK;B?OKe$ae;~ZLr-(fJ1S`+A3Z2yJA~@clau{>u7?XkE03~T z&C5--SS&@)aERc=pscE5wY=Z~b>4Xx8~MBOBY}++rTB7Q^{GN?j@z3I lB7D{CfPuL$uRS$ciuLqA*IdnixYnqs<*NBendh^^><4M3k+%Q< literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--InvoiceItem.create.2.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_invoice--InvoiceItem.create.2.json new file mode 100644 index 0000000000000000000000000000000000000000..3e27f6a83b2040a0b71aab17a241e9c6dd332dec GIT binary patch literal 518 zcmZ9J%TB{U3`O_-iYRN4@+i=-LxKeo4+(ZfC~{|vWHgxx^C+OG|Bju6(vsa=`ySgj z!y?Oa=v=Zfzs#D|vM7oZ(Nyvtt@>s)PeJFi7^3UYi>Oj)A0D6XukLQwH`i<{rydOv zxn8m|eWO!jP@eio&Wd@R4E5P_1gpX8>@?$9_z7l4P*AcFY>c4ZzyusAq{U2hJotA| zQB;$k?ADPIt4+p?PD5MloIb{N7=hOXZX9-Fo7=pjk|gq(K@qzN+M_2Y^9PQH141j0 zvTo{&s#z}2#Lsew-~~|D=T+I%Twr~I_Xgzn{Z74i9_B>;Zv04KBSi_mtm<*AkT!?u n?Ew+KS|(uN-j~<0HCc-7{(r7Gn*VaEQBlWH^N~`|7rVs|alMhj literal 0 HcmV?d00001 diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index 39b59cc4a1..854580b478 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -529,6 +529,274 @@ class StripeTest(StripeTestCase): 'Billed by invoice']: self.assert_in_response(substring, response) + @mock_stripe(tested_timestamp_fields=["created"]) + def test_free_trial_upgrade_by_card(self, *mocks: Mock) -> None: + user = self.example_user("hamlet") + self.login_user(user) + + with self.settings(FREE_TRIAL_MONTHS=2): + response = self.client_get("/upgrade/") + + self.assert_in_success_response(['Pay annually', 'Free Trial', '2 month'], response) + self.assertNotEqual(user.realm.plan_type, Realm.STANDARD) + self.assertFalse(Customer.objects.filter(realm=user.realm).exists()) + + with patch('corporate.lib.stripe.timezone_now', return_value=self.now): + self.upgrade() + + stripe_customer = stripe_get_customer(Customer.objects.get(realm=user.realm).stripe_customer_id) + self.assertEqual(stripe_customer.default_source.id[:5], 'card_') + self.assertEqual(stripe_customer.description, "zulip (Zulip Dev)") + self.assertEqual(stripe_customer.discount, None) + self.assertEqual(stripe_customer.email, user.email) + metadata_dict = dict(stripe_customer.metadata) + self.assertEqual(metadata_dict['realm_str'], 'zulip') + try: + int(metadata_dict['realm_id']) + except ValueError: # nocoverage + raise AssertionError("realm_id is not a number") + + stripe_charges = [charge for charge in stripe.Charge.list(customer=stripe_customer.id)] + self.assertEqual(len(stripe_charges), 0) + + stripe_invoices = [invoice for invoice in stripe.Invoice.list(customer=stripe_customer.id)] + self.assertEqual(len(stripe_invoices), 0) + + customer = Customer.objects.get(stripe_customer_id=stripe_customer.id, realm=user.realm) + plan = CustomerPlan.objects.get( + customer=customer, automanage_licenses=True, + price_per_license=8000, fixed_price=None, discount=None, billing_cycle_anchor=self.now, + billing_schedule=CustomerPlan.ANNUAL, invoiced_through=LicenseLedger.objects.first(), + next_invoice_date=add_months(self.now, 2), tier=CustomerPlan.STANDARD, + status=CustomerPlan.FREE_TRIAL) + LicenseLedger.objects.get( + plan=plan, is_renewal=True, event_time=self.now, licenses=self.seat_count, + licenses_at_next_renewal=self.seat_count) + audit_log_entries = list(RealmAuditLog.objects.filter(acting_user=user) + .values_list('event_type', 'event_time').order_by('id')) + self.assertEqual(audit_log_entries, [ + (RealmAuditLog.STRIPE_CUSTOMER_CREATED, timestamp_to_datetime(stripe_customer.created)), + (RealmAuditLog.STRIPE_CARD_CHANGED, timestamp_to_datetime(stripe_customer.created)), + (RealmAuditLog.CUSTOMER_PLAN_CREATED, self.now), + # TODO: Check for REALM_PLAN_TYPE_CHANGED + # (RealmAuditLog.REALM_PLAN_TYPE_CHANGED, Kandra()), + ]) + self.assertEqual(ujson.loads(RealmAuditLog.objects.filter( + event_type=RealmAuditLog.CUSTOMER_PLAN_CREATED).values_list( + 'extra_data', flat=True).first())['automanage_licenses'], True) + + realm = get_realm("zulip") + self.assertEqual(realm.plan_type, Realm.STANDARD) + self.assertEqual(realm.max_invites, Realm.INVITES_STANDARD_REALM_DAILY_MAX) + + with patch('corporate.views.timezone_now', return_value=self.now): + response = self.client_get("/billing/") + self.assert_not_in_success_response(['Pay annually'], response) + for substring in [ + 'Zulip Standard', 'Free Trial', str(self.seat_count), + 'You are using', '%s of %s licenses' % (self.seat_count, self.seat_count), + 'Your plan will be upgraded to', 'March 2, 2012', '$%s.00' % (80 * self.seat_count,), + 'Visa ending in 4242', + 'Update card']: + self.assert_in_response(substring, response) + + with patch('corporate.lib.stripe.get_latest_seat_count', return_value=12): + update_license_ledger_if_needed(realm, self.now) + self.assertEqual( + LicenseLedger.objects.order_by('-id').values_list('licenses', 'licenses_at_next_renewal').first(), + (12, 12) + ) + + with patch('corporate.lib.stripe.get_latest_seat_count', return_value=15): + update_license_ledger_if_needed(realm, self.next_month) + self.assertEqual( + LicenseLedger.objects.order_by('-id').values_list('licenses', 'licenses_at_next_renewal').first(), + (15, 15) + ) + + invoice_plans_as_needed(self.next_month) + invoices = stripe.Invoice.list(customer=stripe_customer.id) + self.assertEqual(len(invoices), 0) + customer_plan = CustomerPlan.objects.get(customer=customer) + self.assertEqual(customer_plan.status, CustomerPlan.FREE_TRIAL) + self.assertEqual(customer_plan.next_invoice_date, add_months(self.now, 2)) + + invoice_plans_as_needed(add_months(self.now, 2)) + customer_plan.refresh_from_db() + realm.refresh_from_db() + self.assertEqual(customer_plan.status, CustomerPlan.ACTIVE) + self.assertEqual(customer_plan.next_invoice_date, add_months(self.now, 3)) + self.assertEqual(realm.plan_type, Realm.STANDARD) + invoices = [invoice for invoice in stripe.Invoice.list(customer=stripe_customer.id)] + self.assertEqual(len(invoices), 1) + invoice_params = { + "amount_due": 15 * 80 * 100, "amount_paid": 0, "amount_remaining": 15 * 80 * 100, + "auto_advance": True, "billing": "charge_automatically", "collection_method": "charge_automatically", + "customer_email": self.example_email("hamlet"), "discount": None, "paid": False, "status": "open", + "total": 15 * 80 * 100 + } + for key, value in invoice_params.items(): + self.assertEqual(invoices[0].get(key), value) + invoice_items = [invoice_item for invoice_item in invoices[0].get("lines")] + self.assertEqual(len(invoice_items), 1) + invoice_item_params = { + "amount": 15 * 80 * 100, "description": "Zulip Standard - renewal", + "plan": None, "quantity": 15, "subscription": None, "discountable": False, + "period": { + "start": datetime_to_timestamp(add_months(self.now, 2)), + "end": datetime_to_timestamp(add_months(self.now, 14)) + }, + } + for key, value in invoice_item_params.items(): + self.assertEqual(invoice_items[0][key], value) + + invoice_plans_as_needed(add_months(self.now, 3)) + invoices = [invoice for invoice in stripe.Invoice.list(customer=stripe_customer.id)] + self.assertEqual(len(invoices), 1) + + with patch('corporate.lib.stripe.get_latest_seat_count', return_value=19): + update_license_ledger_if_needed(realm, add_months(self.now, 12)) + self.assertEqual( + LicenseLedger.objects.order_by('-id').values_list('licenses', 'licenses_at_next_renewal').first(), + (19, 19) + ) + invoice_plans_as_needed(add_months(self.now, 12)) + invoices = [invoice for invoice in stripe.Invoice.list(customer=stripe_customer.id)] + self.assertEqual(len(invoices), 2) + invoice_params = { + "amount_due": 5172, "auto_advance": True, "billing": "charge_automatically", + "collection_method": "charge_automatically", "customer_email": "hamlet@zulip.com" + } + invoice_items = [invoice_item for invoice_item in invoices[0].get("lines")] + self.assertEqual(len(invoice_items), 1) + invoice_item_params = { + "amount": 5172, "description": "Additional license (Jan 2, 2013 - Mar 2, 2013)", + "discountable": False, "quantity": 4, + "period": { + "start": datetime_to_timestamp(add_months(self.now, 12)), + "end": datetime_to_timestamp(add_months(self.now, 14)) + } + } + + invoice_plans_as_needed(add_months(self.now, 14)) + invoices = [invoice for invoice in stripe.Invoice.list(customer=stripe_customer.id)] + self.assertEqual(len(invoices), 3) + + @mock_stripe(tested_timestamp_fields=["created"]) + def test_free_trial_upgrade_by_invoice(self, *mocks: Mock) -> None: + user = self.example_user("hamlet") + self.login_user(user) + + with self.settings(FREE_TRIAL_MONTHS=2): + response = self.client_get("/upgrade/") + + self.assert_in_success_response(['Pay annually', 'Free Trial', '2 month'], response) + self.assertNotEqual(user.realm.plan_type, Realm.STANDARD) + self.assertFalse(Customer.objects.filter(realm=user.realm).exists()) + + with patch('corporate.lib.stripe.timezone_now', return_value=self.now): + self.upgrade(invoice=True) + + stripe_customer = stripe_get_customer(Customer.objects.get(realm=user.realm).stripe_customer_id) + self.assertEqual(stripe_customer.discount, None) + self.assertEqual(stripe_customer.email, user.email) + metadata_dict = dict(stripe_customer.metadata) + self.assertEqual(metadata_dict['realm_str'], 'zulip') + try: + int(metadata_dict['realm_id']) + except ValueError: # nocoverage + raise AssertionError("realm_id is not a number") + + stripe_invoices = [invoice for invoice in stripe.Invoice.list(customer=stripe_customer.id)] + self.assertEqual(len(stripe_invoices), 0) + + customer = Customer.objects.get(stripe_customer_id=stripe_customer.id, realm=user.realm) + plan = CustomerPlan.objects.get( + customer=customer, automanage_licenses=False, + price_per_license=8000, fixed_price=None, discount=None, billing_cycle_anchor=self.now, + billing_schedule=CustomerPlan.ANNUAL, invoiced_through=LicenseLedger.objects.first(), + next_invoice_date=add_months(self.now, 2), tier=CustomerPlan.STANDARD, + status=CustomerPlan.FREE_TRIAL) + + LicenseLedger.objects.get( + plan=plan, is_renewal=True, event_time=self.now, licenses=123, + licenses_at_next_renewal=123) + audit_log_entries = list(RealmAuditLog.objects.filter(acting_user=user) + .values_list('event_type', 'event_time').order_by('id')) + self.assertEqual(audit_log_entries, [ + (RealmAuditLog.STRIPE_CUSTOMER_CREATED, timestamp_to_datetime(stripe_customer.created)), + (RealmAuditLog.CUSTOMER_PLAN_CREATED, self.now), + # TODO: Check for REALM_PLAN_TYPE_CHANGED + # (RealmAuditLog.REALM_PLAN_TYPE_CHANGED, Kandra()), + ]) + self.assertEqual(ujson.loads(RealmAuditLog.objects.filter( + event_type=RealmAuditLog.CUSTOMER_PLAN_CREATED).values_list( + 'extra_data', flat=True).first())['automanage_licenses'], False) + + realm = get_realm("zulip") + self.assertEqual(realm.plan_type, Realm.STANDARD) + self.assertEqual(realm.max_invites, Realm.INVITES_STANDARD_REALM_DAILY_MAX) + + with patch('corporate.views.timezone_now', return_value=self.now): + response = self.client_get("/billing/") + self.assert_not_in_success_response(['Pay annually'], response) + for substring in [ + 'Zulip Standard', 'Free Trial', str(self.seat_count), + 'You are using', '%s of %s licenses' % (self.seat_count, 123), + 'Your plan will be upgraded to', 'March 2, 2012', + '{:,.2f}'.format(80 * 123), 'Billed by invoice' + ]: + self.assert_in_response(substring, response) + + with patch('corporate.lib.stripe.invoice_plan') as mocked: + invoice_plans_as_needed(self.next_month) + mocked.assert_not_called() + mocked.reset_mock() + customer_plan = CustomerPlan.objects.get(customer=customer) + self.assertEqual(customer_plan.status, CustomerPlan.FREE_TRIAL) + self.assertEqual(customer_plan.next_invoice_date, add_months(self.now, 2)) + + invoice_plans_as_needed(add_months(self.now, 2)) + customer_plan.refresh_from_db() + realm.refresh_from_db() + self.assertEqual(customer_plan.status, CustomerPlan.ACTIVE) + self.assertEqual(customer_plan.next_invoice_date, add_months(self.now, 14)) + self.assertEqual(realm.plan_type, Realm.STANDARD) + invoices = [invoice for invoice in stripe.Invoice.list(customer=stripe_customer.id)] + self.assertEqual(len(invoices), 1) + invoice_params = { + "amount_due": 123 * 80 * 100, "amount_paid": 0, "amount_remaining": 123 * 80 * 100, + "auto_advance": True, "billing": "send_invoice", "collection_method": "send_invoice", + "customer_email": self.example_email("hamlet"), "discount": None, "paid": False, "status": "open", + "total": 123 * 80 * 100 + } + for key, value in invoice_params.items(): + self.assertEqual(invoices[0].get(key), value) + invoice_items = [invoice_item for invoice_item in invoices[0].get("lines")] + self.assertEqual(len(invoice_items), 1) + invoice_item_params = { + "amount": 123 * 80 * 100, "description": "Zulip Standard - renewal", + "plan": None, "quantity": 123, "subscription": None, "discountable": False, + "period": { + "start": datetime_to_timestamp(add_months(self.now, 2)), + "end": datetime_to_timestamp(add_months(self.now, 14)) + }, + } + for key, value in invoice_item_params.items(): + self.assertEqual(invoice_items[0][key], value) + + invoice_plans_as_needed(add_months(self.now, 3)) + invoices = [invoice for invoice in stripe.Invoice.list(customer=stripe_customer.id)] + self.assertEqual(len(invoices), 1) + + invoice_plans_as_needed(add_months(self.now, 12)) + invoices = [invoice for invoice in stripe.Invoice.list(customer=stripe_customer.id)] + self.assertEqual(len(invoices), 1) + + invoice_plans_as_needed(add_months(self.now, 14)) + invoices = [invoice for invoice in stripe.Invoice.list(customer=stripe_customer.id)] + self.assertEqual(len(invoices), 2) + @mock_stripe() def test_billing_page_permissions(self, *mocks: Mock) -> None: hamlet = self.example_user('hamlet') @@ -972,6 +1240,22 @@ class StripeTest(StripeTestCase): invoice_plans_as_needed(self.next_year + timedelta(days=400)) mocked.assert_not_called() + @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") + self.login_user(user) + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): + self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, 'token') + response = self.client_post("/json/billing/plan/change", + {'status': CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE}) + self.assert_json_success(response) + self.assertEqual(CustomerPlan.objects.first().status, CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE) + + response = self.client_post("/json/billing/plan/change", + {'status': CustomerPlan.ACTIVE}) + self.assert_json_success(response) + self.assertEqual(CustomerPlan.objects.first().status, CustomerPlan.ACTIVE) + @patch("corporate.lib.stripe.billing_logger.info") @patch("stripe.Invoice.create") @patch("stripe.Invoice.finalize_invoice") @@ -997,6 +1281,49 @@ class StripeTest(StripeTestCase): self.assertIsNone(plan.next_invoice_date) self.assertEqual(plan.status, CustomerPlan.ENDED) + @patch("corporate.lib.stripe.billing_logger.info") + def test_downgrade_free_trial(self, mock_: Mock) -> None: + user = self.example_user("hamlet") + with self.settings(FREE_TRIAL_MONTHS=2): + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): + self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, 'token') + + plan = CustomerPlan.objects.get() + self.assertEqual(plan.next_invoice_date, add_months(self.now, 2)) + self.assertEqual(get_realm('zulip').plan_type, Realm.STANDARD) + self.assertEqual(plan.status, CustomerPlan.FREE_TRIAL) + + # Add some extra users before the realm is deactivated + with patch("corporate.lib.stripe.get_latest_seat_count", return_value=21): + update_license_ledger_if_needed(user.realm, self.now) + + last_ledger_entry = LicenseLedger.objects.order_by('id').last() + self.assertEqual(last_ledger_entry.licenses, 21) + self.assertEqual(last_ledger_entry.licenses_at_next_renewal, 21) + + self.login_user(user) + self.client_post("/json/billing/plan/change", {'status': CustomerPlan.ENDED}) + + plan.refresh_from_db() + self.assertEqual(get_realm('zulip').plan_type, Realm.LIMITED) + self.assertEqual(plan.status, CustomerPlan.ENDED) + self.assertEqual(plan.invoiced_through, last_ledger_entry) + self.assertIsNone(plan.next_invoice_date) + + self.login_user(user) + response = self.client_get("/billing/") + self.assert_in_success_response(["Your organization is on the Zulip Free"], response) + + # The extra users added in the final month are not charged + with patch("corporate.lib.stripe.invoice_plan") as mocked: + invoice_plans_as_needed(self.next_month) + mocked.assert_not_called() + + # The plan is not renewed after an year + with patch("corporate.lib.stripe.invoice_plan") as mocked: + invoice_plans_as_needed(self.next_year) + mocked.assert_not_called() + @patch("corporate.lib.stripe.billing_logger.warning") @patch("corporate.lib.stripe.billing_logger.info") def test_reupgrade_by_billing_admin_after_downgrade(self, *mocks: Mock) -> None: diff --git a/corporate/views.py b/corporate/views.py index c7b9690f0c..edf2cb9ea9 100644 --- a/corporate/views.py +++ b/corporate/views.py @@ -22,7 +22,7 @@ from corporate.lib.stripe import STRIPE_PUBLISHABLE_KEY, \ unsign_string, BillingError, do_change_plan_status, do_replace_payment_source, \ MIN_INVOICED_LICENSES, MAX_INVOICED_LICENSES, DEFAULT_INVOICE_DAYS_UNTIL_DUE, \ start_of_next_billing_cycle, renewal_amount, \ - make_end_of_cycle_updates_if_needed + make_end_of_cycle_updates_if_needed, downgrade_now from corporate.models import CustomerPlan, get_current_plan_by_customer, \ get_customer_by_realm, get_current_plan_by_realm @@ -146,6 +146,7 @@ def initial_upgrade(request: HttpRequest) -> HttpResponse: 'min_invoiced_licenses': max(seat_count, MIN_INVOICED_LICENSES), 'default_invoice_days_until_due': DEFAULT_INVOICE_DAYS_UNTIL_DUE, 'plan': "Zulip Standard", + "free_trial_months": settings.FREE_TRIAL_MONTHS, 'page_params': { 'seat_count': seat_count, 'annual_price': 8000, @@ -183,6 +184,7 @@ def billing_home(request: HttpRequest) -> HttpResponse: CustomerPlan.STANDARD: 'Zulip Standard', CustomerPlan.PLUS: 'Zulip Plus', }[plan.tier] + free_trial = plan.status == CustomerPlan.FREE_TRIAL licenses = last_ledger_entry.licenses licenses_used = get_latest_seat_count(user.realm) # Should do this in javascript, using the user's timezone @@ -198,6 +200,7 @@ def billing_home(request: HttpRequest) -> HttpResponse: context.update({ 'plan_name': plan_name, 'has_active_plan': True, + 'free_trial': free_trial, 'licenses': licenses, 'licenses_used': licenses_used, 'renewal_date': renewal_date, @@ -214,9 +217,20 @@ 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]) + plan = get_current_plan_by_realm(user.realm) assert(plan is not None) # for mypy - do_change_plan_status(plan, status) + + if status == CustomerPlan.ACTIVE: + assert(plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE) + do_change_plan_status(plan, status) + elif status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE: + assert(plan.status == CustomerPlan.ACTIVE) + do_change_plan_status(plan, status) + elif status == CustomerPlan.ENDED: + assert(plan.status == CustomerPlan.FREE_TRIAL) + downgrade_now(user.realm) return json_success() @require_billing_access diff --git a/templates/corporate/billing.html b/templates/corporate/billing.html index d57e63edab..bf53b58124 100644 --- a/templates/corporate/billing.html +++ b/templates/corporate/billing.html @@ -28,14 +28,23 @@
+ {% if free_trial %} +

Your current plan is {{ plan_name }} Free Trial.

+ {% else %}

Your current plan is {{ plan_name }}.

+ {% endif %}

You are using {{ licenses_used }} of {{ licenses }} licenses.

{% if renewal_amount %} - Your plan will renew on {{ renewal_date }} for - ${{ renewal_amount }}. + {% if free_trial %} + Your plan will be upgraded to {{ plan_name }} on {{ renewal_date }} for + ${{ renewal_amount }}. + {% else %} + Your plan will renew on {{ renewal_date }} for + ${{ renewal_amount }}. + {% endif %} {% else %} - Your plan ends on {{ renewal_date }}, and does not renew. + Your plan ends on {{ renewal_date }}, and does not renew. {% endif %}

diff --git a/templates/corporate/upgrade.html b/templates/corporate/upgrade.html index baec13a365..1749b42688 100644 --- a/templates/corporate/upgrade.html +++ b/templates/corporate/upgrade.html @@ -18,6 +18,12 @@

{% trans %}Upgrade to {{ plan }}{% endtrans %}

+ {% if free_trial_months %} +
+ Upgrade now to start your {{ free_trial_months }} month Free Trial of Zulip Standard. +
+ {% endif %} + {% if error_message %}
{{ error_message }} @@ -38,6 +44,12 @@ + {% if free_trial_months %} +

+ You won't be charged during the Free Trial. You can also downgrade back to Zulip Limited + during the Free Trial. +

+ {% endif %}

{{ _("Payment schedule") }}

@@ -82,21 +94,37 @@

+ {% if free_trial_months %} + After the Free Trial, you’ll be charged + $ for {{ seat_count }} + users. + We'll automatically charge you for additional licenses as users + are added, and remove licenses not in use at the end of each billing + period. + {% else %} You’ll initially be charged $ for {{ seat_count }} users.
We'll automatically charge you for additional licenses as users are added, and remove licenses not in use at the end of each billing period. + {% endif %}

+

+ {% if free_trial_months %} + Enter the number of users you would like to pay for after the Free Trial.
+ You'll need to manually add licenses to add or invite + additional users. + {% else %} Enter the number of users you would like to pay for.
You'll need to manually add licenses to add or invite additional users. + {% endif %}

Number of licenses (minimum {{ seat_count }})

@@ -147,9 +175,15 @@

+ {% if free_trial_months %} + Enter the number of users you would like to pay for.
+ We'll email you an invoice after the Free Trial. + Invoices can be paid by ACH transfer or credit card. + {% else %} Enter the number of users you would like to pay for.
We'll email you an invoice in 1-2 hours. Invoices can be paid by ACH transfer or credit card. + {% endif %}

Number of licenses (minimum {{ min_invoiced_licenses }})

{% elif realm_plan_type == 2 %}
+ {% if free_trial_months %} + Start {{ free_trial_months }} month Free Trial + {% else %} Buy Standard + {% endif %} {% else %} + {% if free_trial_months %} + Start {{ free_trial_months }} month Free Trial + {% else %} Buy Standard + {% endif %} {% endif %}
diff --git a/tools/check-templates b/tools/check-templates index 6eb07953b4..c028672cce 100755 --- a/tools/check-templates +++ b/tools/check-templates @@ -68,6 +68,7 @@ def check_html_templates(templates: Iterable[str], all_dups: bool, fix: bool) -> 'send_confirm', 'register', 'footer', + 'charged_amount', # Temporary while we have searchbox forked 'search_exit', 'search_query', diff --git a/zerver/views/portico.py b/zerver/views/portico.py index c1de237190..0c08aaa0ed 100644 --- a/zerver/views/portico.py +++ b/zerver/views/portico.py @@ -21,13 +21,15 @@ def apps_view(request: HttpRequest, _: str) -> HttpResponse: def plans_view(request: HttpRequest) -> HttpResponse: realm = get_realm_from_request(request) realm_plan_type = 0 + free_trial_months = settings.FREE_TRIAL_MONTHS if realm is not None: realm_plan_type = realm.plan_type if realm.plan_type == Realm.SELF_HOSTED and settings.PRODUCTION: return HttpResponseRedirect('https://zulipchat.com/plans') if not request.user.is_authenticated: return redirect_to_login(next="plans") - return render(request, "zerver/plans.html", context={"realm_plan_type": realm_plan_type}) + return render(request, "zerver/plans.html", + context={"realm_plan_type": realm_plan_type, 'free_trial_months': free_trial_months}) def team_view(request: HttpRequest) -> HttpResponse: if not settings.ZILENCER_ENABLED: diff --git a/zproject/default_settings.py b/zproject/default_settings.py index 4daae03beb..5402d16376 100644 --- a/zproject/default_settings.py +++ b/zproject/default_settings.py @@ -363,6 +363,8 @@ ARCHIVED_DATA_VACUUMING_DELAY_DAYS = 7 # are available to all realms. BILLING_ENABLED = False +FREE_TRIAL_MONTHS = None + # Automatically catch-up soft deactivated users when running the # `soft-deactivate-users` cron. Turn this off if the server has 10Ks of # users, and you would like to save some disk space. Soft-deactivated diff --git a/zproject/dev_settings.py b/zproject/dev_settings.py index 63b790465b..d970286603 100644 --- a/zproject/dev_settings.py +++ b/zproject/dev_settings.py @@ -155,6 +155,7 @@ THUMBNAIL_IMAGES = True SEARCH_PILLS_ENABLED = bool(os.getenv('SEARCH_PILLS_ENABLED', False)) BILLING_ENABLED = True +FREE_TRIAL_MONTHS = None # Test Custom TOS template rendering TERMS_OF_SERVICE = 'corporate/terms.md'