diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index 64016862db..5c045e67e3 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -91,6 +91,21 @@ def next_renewal_date(plan: CustomerPlan, event_time: datetime) -> datetime: periods += 1 return dt +# TODO take downgrade into account +def next_invoice_date(plan: CustomerPlan) -> datetime: + months_per_period = { + CustomerPlan.ANNUAL: 12, + CustomerPlan.MONTHLY: 1, + }[plan.billing_schedule] + if plan.automanage_licenses: + months_per_period = 1 + periods = 1 + dt = plan.billing_cycle_anchor + while dt <= plan.next_invoice_date: + dt = add_months(plan.billing_cycle_anchor, months_per_period * periods) + periods += 1 + return dt + def renewal_amount(plan: CustomerPlan) -> Optional[int]: # nocoverage: TODO if plan.fixed_price is not None: return plan.fixed_price @@ -356,6 +371,77 @@ def update_license_ledger_if_needed(realm: Realm, event_time: datetime) -> None: return update_license_ledger_for_automanaged_plan(realm, plan, event_time) +def invoice_plan(plan: CustomerPlan, event_time: datetime) -> None: + if plan.invoicing_status == CustomerPlan.STARTED: + raise NotImplementedError('Plan with invoicing_status==STARTED needs manual resolution.') + add_plan_renewal_to_license_ledger_if_needed(plan, event_time) + assert(plan.invoiced_through is not None) + licenses_base = plan.invoiced_through.licenses + invoice_item_created = False + for ledger_entry in LicenseLedger.objects.filter(plan=plan, id__gt=plan.invoiced_through.id, + event_time__lte=event_time).order_by('id'): + price_args = {} # type: Dict[str, int] + if ledger_entry.is_renewal: + if plan.fixed_price is not None: + price_args = {'amount': plan.fixed_price} + else: + assert(plan.price_per_license is not None) # needed for mypy + price_args = {'unit_amount': plan.price_per_license, + 'quantity': ledger_entry.licenses} + description = "Zulip Standard - renewal" + elif ledger_entry.licenses != licenses_base: + assert(plan.price_per_license) + last_renewal = LicenseLedger.objects.filter( + plan=plan, is_renewal=True, event_time__lte=ledger_entry.event_time) \ + .order_by('-id').first().event_time + period_end = next_renewal_date(plan, ledger_entry.event_time) + proration_fraction = (period_end - ledger_entry.event_time) / (period_end - last_renewal) + price_args = {'unit_amount': int(plan.price_per_license * proration_fraction + .5), + 'quantity': ledger_entry.licenses - licenses_base} + description = "Additional license ({} - {})".format( + ledger_entry.event_time.strftime('%b %-d, %Y'), period_end.strftime('%b %-d, %Y')) + + if price_args: + plan.invoiced_through = ledger_entry + plan.invoicing_status = CustomerPlan.STARTED + plan.save(update_fields=['invoicing_status', 'invoiced_through']) + stripe.InvoiceItem.create( + currency='usd', + customer=plan.customer.stripe_customer_id, + description=description, + discountable=False, + period = {'start': datetime_to_timestamp(ledger_entry.event_time), + 'end': datetime_to_timestamp(next_renewal_date(plan, ledger_entry.event_time))}, + idempotency_key='ledger_entry:{}'.format(ledger_entry.id), + **price_args) + invoice_item_created = True + plan.invoiced_through = ledger_entry + plan.invoicing_status = CustomerPlan.DONE + plan.save(update_fields=['invoicing_status', 'invoiced_through']) + licenses_base = ledger_entry.licenses + + if invoice_item_created: + if plan.charge_automatically: + billing_method = 'charge_automatically' + days_until_due = None + else: + billing_method = 'send_invoice' + days_until_due = DEFAULT_INVOICE_DAYS_UNTIL_DUE + stripe_invoice = stripe.Invoice.create( + auto_advance=True, + billing=billing_method, + customer=plan.customer.stripe_customer_id, + days_until_due=days_until_due, + statement_descriptor='Zulip Standard') + stripe.Invoice.finalize_invoice(stripe_invoice) + + plan.next_invoice_date = next_invoice_date(plan) + plan.save(update_fields=['next_invoice_date']) + +def invoice_plans_as_needed(event_time: datetime) -> None: + for plan in CustomerPlan.objects.filter(next_invoice_date__lte=event_time): + invoice_plan(plan, event_time) + def attach_discount_to_realm(user: UserProfile, discount: Decimal) -> None: customer = Customer.objects.filter(realm=user.realm).first() if customer is None: diff --git a/corporate/tests/stripe_fixtures/fixed_price_plans:Customer.create.1.json b/corporate/tests/stripe_fixtures/fixed_price_plans:Customer.create.1.json new file mode 100644 index 0000000000..e82680e46b --- /dev/null +++ b/corporate/tests/stripe_fixtures/fixed_price_plans:Customer.create.1.json @@ -0,0 +1,39 @@ +{ + "account_balance": 0, + "created": 1000000000, + "currency": null, + "default_source": null, + "delinquent": false, + "description": "zulip (Zulip Dev)", + "discount": null, + "email": "hamlet@zulip.com", + "id": "cus_NORMALIZED0001", + "invoice_prefix": "NORMA01", + "invoice_settings": { + "custom_fields": null, + "footer": null + }, + "livemode": false, + "metadata": { + "realm_id": "1", + "realm_str": "zulip" + }, + "object": "customer", + "shipping": null, + "sources": { + "data": [], + "has_more": false, + "object": "list", + "total_count": 0, + "url": "/v1/customers/cus_NORMALIZED0001/sources" + }, + "subscriptions": { + "data": [], + "has_more": false, + "object": "list", + "total_count": 0, + "url": "/v1/customers/cus_NORMALIZED0001/subscriptions" + }, + "tax_info": null, + "tax_info_verification": null +} diff --git a/corporate/tests/stripe_fixtures/fixed_price_plans:Invoice.create.1.json b/corporate/tests/stripe_fixtures/fixed_price_plans:Invoice.create.1.json new file mode 100644 index 0000000000..af1ae1bcd7 --- /dev/null +++ b/corporate/tests/stripe_fixtures/fixed_price_plans:Invoice.create.1.json @@ -0,0 +1,72 @@ +{ + "amount_due": 984000, + "amount_paid": 0, + "amount_remaining": 984000, + "application_fee": null, + "attempt_count": 0, + "attempted": false, + "auto_advance": true, + "billing": "send_invoice", + "billing_reason": "manual", + "charge": null, + "currency": "usd", + "custom_fields": null, + "customer": "cus_NORMALIZED0001", + "date": 1000000000, + "default_source": null, + "description": "", + "discount": null, + "due_date": 1000000000, + "ending_balance": null, + "finalized_at": null, + "footer": null, + "hosted_invoice_url": null, + "id": "in_NORMALIZED00000000000001", + "invoice_pdf": null, + "lines": { + "data": [ + { + "amount": 984000, + "currency": "usd", + "description": "Zulip Standard", + "discountable": false, + "id": "ii_NORMALIZED00000000000001", + "invoice_item": "ii_NORMALIZED00000000000001", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1357095845, + "start": 1325473445 + }, + "plan": null, + "proration": false, + "quantity": 123, + "subscription": null, + "type": "invoiceitem" + } + ], + "has_more": false, + "object": "list", + "total_count": 1, + "url": "/v1/invoices/in_NORMALIZED00000000000001/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "number": "NORMALI-0001", + "object": "invoice", + "paid": false, + "period_end": 1000000000, + "period_start": 1000000000, + "receipt_number": null, + "starting_balance": 0, + "statement_descriptor": "Zulip Standard", + "status": "draft", + "subscription": null, + "subtotal": 984000, + "tax": 0, + "tax_percent": null, + "total": 984000, + "webhooks_delivered_at": null +} diff --git a/corporate/tests/stripe_fixtures/fixed_price_plans:Invoice.create.2.json b/corporate/tests/stripe_fixtures/fixed_price_plans:Invoice.create.2.json new file mode 100644 index 0000000000..be48298dc4 --- /dev/null +++ b/corporate/tests/stripe_fixtures/fixed_price_plans:Invoice.create.2.json @@ -0,0 +1,72 @@ +{ + "amount_due": 100, + "amount_paid": 0, + "amount_remaining": 100, + "application_fee": null, + "attempt_count": 0, + "attempted": false, + "auto_advance": true, + "billing": "send_invoice", + "billing_reason": "manual", + "charge": null, + "currency": "usd", + "custom_fields": null, + "customer": "cus_NORMALIZED0001", + "date": 1000000000, + "default_source": null, + "description": "", + "discount": null, + "due_date": 1000000000, + "ending_balance": null, + "finalized_at": null, + "footer": null, + "hosted_invoice_url": null, + "id": "in_NORMALIZED00000000000002", + "invoice_pdf": null, + "lines": { + "data": [ + { + "amount": 100, + "currency": "usd", + "description": "Zulip Standard - renewal", + "discountable": false, + "id": "ii_NORMALIZED00000000000002", + "invoice_item": "ii_NORMALIZED00000000000002", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1388631845, + "start": 1357095845 + }, + "plan": null, + "proration": false, + "quantity": 1, + "subscription": null, + "type": "invoiceitem" + } + ], + "has_more": false, + "object": "list", + "total_count": 1, + "url": "/v1/invoices/in_NORMALIZED00000000000002/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "number": "NORMALI-0002", + "object": "invoice", + "paid": false, + "period_end": 1000000000, + "period_start": 1000000000, + "receipt_number": null, + "starting_balance": 0, + "statement_descriptor": "Zulip Standard", + "status": "draft", + "subscription": null, + "subtotal": 100, + "tax": 0, + "tax_percent": null, + "total": 100, + "webhooks_delivered_at": null +} diff --git a/corporate/tests/stripe_fixtures/fixed_price_plans:Invoice.finalize_invoice.1.json b/corporate/tests/stripe_fixtures/fixed_price_plans:Invoice.finalize_invoice.1.json new file mode 100644 index 0000000000..2d10dc24af --- /dev/null +++ b/corporate/tests/stripe_fixtures/fixed_price_plans:Invoice.finalize_invoice.1.json @@ -0,0 +1,72 @@ +{ + "amount_due": 984000, + "amount_paid": 0, + "amount_remaining": 984000, + "application_fee": null, + "attempt_count": 0, + "attempted": false, + "auto_advance": true, + "billing": "send_invoice", + "billing_reason": "manual", + "charge": null, + "currency": "usd", + "custom_fields": null, + "customer": "cus_NORMALIZED0001", + "date": 1000000000, + "default_source": null, + "description": "", + "discount": null, + "due_date": 1000000000, + "ending_balance": 0, + "finalized_at": 1000000000, + "footer": null, + "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001", + "id": "in_NORMALIZED00000000000001", + "invoice_pdf": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001/pdf", + "lines": { + "data": [ + { + "amount": 984000, + "currency": "usd", + "description": "Zulip Standard", + "discountable": false, + "id": "ii_NORMALIZED00000000000001", + "invoice_item": "ii_NORMALIZED00000000000001", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1357095845, + "start": 1325473445 + }, + "plan": null, + "proration": false, + "quantity": 123, + "subscription": null, + "type": "invoiceitem" + } + ], + "has_more": false, + "object": "list", + "total_count": 1, + "url": "/v1/invoices/in_NORMALIZED00000000000001/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "number": "NORMALI-0001", + "object": "invoice", + "paid": false, + "period_end": 1000000000, + "period_start": 1000000000, + "receipt_number": null, + "starting_balance": 0, + "statement_descriptor": "Zulip Standard", + "status": "open", + "subscription": null, + "subtotal": 984000, + "tax": 0, + "tax_percent": null, + "total": 984000, + "webhooks_delivered_at": 1000000000 +} diff --git a/corporate/tests/stripe_fixtures/fixed_price_plans:Invoice.finalize_invoice.2.json b/corporate/tests/stripe_fixtures/fixed_price_plans:Invoice.finalize_invoice.2.json new file mode 100644 index 0000000000..c615fb8424 --- /dev/null +++ b/corporate/tests/stripe_fixtures/fixed_price_plans:Invoice.finalize_invoice.2.json @@ -0,0 +1,72 @@ +{ + "amount_due": 100, + "amount_paid": 0, + "amount_remaining": 100, + "application_fee": null, + "attempt_count": 0, + "attempted": false, + "auto_advance": true, + "billing": "send_invoice", + "billing_reason": "manual", + "charge": null, + "currency": "usd", + "custom_fields": null, + "customer": "cus_NORMALIZED0001", + "date": 1000000000, + "default_source": null, + "description": "", + "discount": null, + "due_date": 1000000000, + "ending_balance": 0, + "finalized_at": 1000000000, + "footer": null, + "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000002", + "id": "in_NORMALIZED00000000000002", + "invoice_pdf": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000002/pdf", + "lines": { + "data": [ + { + "amount": 100, + "currency": "usd", + "description": "Zulip Standard - renewal", + "discountable": false, + "id": "ii_NORMALIZED00000000000002", + "invoice_item": "ii_NORMALIZED00000000000002", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1388631845, + "start": 1357095845 + }, + "plan": null, + "proration": false, + "quantity": 1, + "subscription": null, + "type": "invoiceitem" + } + ], + "has_more": false, + "object": "list", + "total_count": 1, + "url": "/v1/invoices/in_NORMALIZED00000000000002/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "number": "NORMALI-0002", + "object": "invoice", + "paid": false, + "period_end": 1000000000, + "period_start": 1000000000, + "receipt_number": null, + "starting_balance": 0, + "statement_descriptor": "Zulip Standard", + "status": "open", + "subscription": null, + "subtotal": 100, + "tax": 0, + "tax_percent": null, + "total": 100, + "webhooks_delivered_at": 1000000000 +} diff --git a/corporate/tests/stripe_fixtures/fixed_price_plans:Invoice.list.1.json b/corporate/tests/stripe_fixtures/fixed_price_plans:Invoice.list.1.json new file mode 100644 index 0000000000..0655abde17 --- /dev/null +++ b/corporate/tests/stripe_fixtures/fixed_price_plans:Invoice.list.1.json @@ -0,0 +1,151 @@ +{ + "data": [ + { + "amount_due": 100, + "amount_paid": 0, + "amount_remaining": 100, + "application_fee": null, + "attempt_count": 0, + "attempted": false, + "auto_advance": true, + "billing": "send_invoice", + "billing_reason": "manual", + "charge": null, + "currency": "usd", + "custom_fields": null, + "customer": "cus_NORMALIZED0001", + "date": 1000000000, + "default_source": null, + "description": "", + "discount": null, + "due_date": 1000000000, + "ending_balance": 0, + "finalized_at": 1000000000, + "footer": null, + "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000002", + "id": "in_NORMALIZED00000000000002", + "invoice_pdf": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000002/pdf", + "lines": { + "data": [ + { + "amount": 100, + "currency": "usd", + "description": "Zulip Standard - renewal", + "discountable": false, + "id": "ii_NORMALIZED00000000000002", + "invoice_item": "ii_NORMALIZED00000000000002", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1388631845, + "start": 1357095845 + }, + "plan": null, + "proration": false, + "quantity": 1, + "subscription": null, + "type": "invoiceitem" + } + ], + "has_more": false, + "object": "list", + "total_count": 1, + "url": "/v1/invoices/in_NORMALIZED00000000000002/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "number": "NORMALI-0002", + "object": "invoice", + "paid": false, + "period_end": 1000000000, + "period_start": 1000000000, + "receipt_number": null, + "starting_balance": 0, + "statement_descriptor": "Zulip Standard", + "status": "open", + "subscription": null, + "subtotal": 100, + "tax": 0, + "tax_percent": null, + "total": 100, + "webhooks_delivered_at": 1000000000 + }, + { + "amount_due": 984000, + "amount_paid": 0, + "amount_remaining": 984000, + "application_fee": null, + "attempt_count": 0, + "attempted": false, + "auto_advance": true, + "billing": "send_invoice", + "billing_reason": "manual", + "charge": null, + "currency": "usd", + "custom_fields": null, + "customer": "cus_NORMALIZED0001", + "date": 1000000000, + "default_source": null, + "description": "", + "discount": null, + "due_date": 1000000000, + "ending_balance": 0, + "finalized_at": 1000000000, + "footer": null, + "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001", + "id": "in_NORMALIZED00000000000001", + "invoice_pdf": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001/pdf", + "lines": { + "data": [ + { + "amount": 984000, + "currency": "usd", + "description": "Zulip Standard", + "discountable": false, + "id": "ii_NORMALIZED00000000000001", + "invoice_item": "ii_NORMALIZED00000000000001", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1357095845, + "start": 1325473445 + }, + "plan": null, + "proration": false, + "quantity": 123, + "subscription": null, + "type": "invoiceitem" + } + ], + "has_more": false, + "object": "list", + "total_count": 1, + "url": "/v1/invoices/in_NORMALIZED00000000000001/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "number": "NORMALI-0001", + "object": "invoice", + "paid": false, + "period_end": 1000000000, + "period_start": 1000000000, + "receipt_number": null, + "starting_balance": 0, + "statement_descriptor": "Zulip Standard", + "status": "open", + "subscription": null, + "subtotal": 984000, + "tax": 0, + "tax_percent": null, + "total": 984000, + "webhooks_delivered_at": 1000000000 + } + ], + "has_more": false, + "object": "list", + "url": "/v1/invoices" +} diff --git a/corporate/tests/stripe_fixtures/fixed_price_plans:InvoiceItem.create.1.json b/corporate/tests/stripe_fixtures/fixed_price_plans:InvoiceItem.create.1.json new file mode 100644 index 0000000000..5075cb0ccd --- /dev/null +++ b/corporate/tests/stripe_fixtures/fixed_price_plans:InvoiceItem.create.1.json @@ -0,0 +1,22 @@ +{ + "amount": 984000, + "currency": "usd", + "customer": "cus_NORMALIZED0001", + "date": 1000000000, + "description": "Zulip Standard", + "discountable": false, + "id": "ii_NORMALIZED00000000000001", + "invoice": null, + "livemode": false, + "metadata": {}, + "object": "invoiceitem", + "period": { + "end": 1357095845, + "start": 1325473445 + }, + "plan": null, + "proration": false, + "quantity": 123, + "subscription": null, + "unit_amount": 8000 +} diff --git a/corporate/tests/stripe_fixtures/fixed_price_plans:InvoiceItem.create.2.json b/corporate/tests/stripe_fixtures/fixed_price_plans:InvoiceItem.create.2.json new file mode 100644 index 0000000000..0662600fe8 --- /dev/null +++ b/corporate/tests/stripe_fixtures/fixed_price_plans:InvoiceItem.create.2.json @@ -0,0 +1,22 @@ +{ + "amount": 100, + "currency": "usd", + "customer": "cus_NORMALIZED0001", + "date": 1000000000, + "description": "Zulip Standard - renewal", + "discountable": false, + "id": "ii_NORMALIZED00000000000002", + "invoice": null, + "livemode": false, + "metadata": {}, + "object": "invoiceitem", + "period": { + "end": 1388631845, + "start": 1357095845 + }, + "plan": null, + "proration": false, + "quantity": 1, + "subscription": null, + "unit_amount": 100 +} diff --git a/corporate/tests/stripe_fixtures/invoice_plan:Charge.create.1.json b/corporate/tests/stripe_fixtures/invoice_plan:Charge.create.1.json new file mode 100644 index 0000000000..54bd5a6f82 --- /dev/null +++ b/corporate/tests/stripe_fixtures/invoice_plan:Charge.create.1.json @@ -0,0 +1,78 @@ +{ + "amount": 64000, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": "txn_1DxdruGh0CmXqmnwJQIcAlGj", + "captured": true, + "created": 1548696358, + "currency": "usd", + "customer": "cus_EQUrCdXCP2oqIF", + "description": "Upgrade to Zulip Standard, $80.0 x 8", + "destination": null, + "dispute": null, + "failure_code": null, + "failure_message": null, + "fraud_details": {}, + "id": "ch_1DxdruGh0CmXqmnwSwEPHAI6", + "invoice": null, + "livemode": false, + "metadata": {}, + "object": "charge", + "on_behalf_of": null, + "order": null, + "outcome": { + "network_status": "approved_by_network", + "reason": null, + "risk_level": "normal", + "risk_score": 6, + "seller_message": "Payment complete.", + "type": "authorized" + }, + "paid": true, + "payment_intent": null, + "receipt_email": "hamlet@zulip.com", + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/acct_1BWYgHGh0CmXqmnw/ch_1DxdruGh0CmXqmnwSwEPHAI6/rcpt_EQUr7TAzRbJU3vjRTVdNKpnyUvWTXz2", + "refunded": false, + "refunds": { + "data": [], + "has_more": false, + "object": "list", + "total_count": 0, + "url": "/v1/charges/ch_1DxdruGh0CmXqmnwSwEPHAI6/refunds" + }, + "review": null, + "shipping": null, + "source": { + "address_city": "Pacific", + "address_country": "United States", + "address_line1": "Under the sea,", + "address_line1_check": "pass", + "address_line2": null, + "address_state": null, + "address_zip": "33333", + "address_zip_check": "pass", + "brand": "Visa", + "country": "US", + "customer": "cus_EQUrCdXCP2oqIF", + "cvc_check": "pass", + "dynamic_last4": null, + "exp_month": 3, + "exp_year": 2033, + "fingerprint": "6dAXT9VZvwro65EK", + "funding": "credit", + "id": "card_1DxdrtGh0CmXqmnw1LvZ0cL4", + "last4": "4242", + "metadata": {}, + "name": "Ada Starr", + "object": "card", + "tokenization_method": null + }, + "source_transfer": null, + "statement_descriptor": "Zulip Standard", + "status": "succeeded", + "transfer_data": null, + "transfer_group": null +} diff --git a/corporate/tests/stripe_fixtures/invoice_plan:Customer.create.1.json b/corporate/tests/stripe_fixtures/invoice_plan:Customer.create.1.json new file mode 100644 index 0000000000..2ba6926aca --- /dev/null +++ b/corporate/tests/stripe_fixtures/invoice_plan:Customer.create.1.json @@ -0,0 +1,65 @@ +{ + "account_balance": 0, + "created": 1548696357, + "currency": null, + "default_source": "card_1DxdrtGh0CmXqmnw1LvZ0cL4", + "delinquent": false, + "description": "zulip (Zulip Dev)", + "discount": null, + "email": "hamlet@zulip.com", + "id": "cus_EQUrCdXCP2oqIF", + "invoice_prefix": "81F7A21", + "invoice_settings": { + "custom_fields": null, + "footer": null + }, + "livemode": false, + "metadata": { + "realm_id": "1", + "realm_str": "zulip" + }, + "object": "customer", + "shipping": null, + "sources": { + "data": [ + { + "address_city": "Pacific", + "address_country": "United States", + "address_line1": "Under the sea,", + "address_line1_check": "pass", + "address_line2": null, + "address_state": null, + "address_zip": "33333", + "address_zip_check": "pass", + "brand": "Visa", + "country": "US", + "customer": "cus_EQUrCdXCP2oqIF", + "cvc_check": "pass", + "dynamic_last4": null, + "exp_month": 3, + "exp_year": 2033, + "fingerprint": "6dAXT9VZvwro65EK", + "funding": "credit", + "id": "card_1DxdrtGh0CmXqmnw1LvZ0cL4", + "last4": "4242", + "metadata": {}, + "name": "Ada Starr", + "object": "card", + "tokenization_method": null + } + ], + "has_more": false, + "object": "list", + "total_count": 1, + "url": "/v1/customers/cus_EQUrCdXCP2oqIF/sources" + }, + "subscriptions": { + "data": [], + "has_more": false, + "object": "list", + "total_count": 0, + "url": "/v1/customers/cus_EQUrCdXCP2oqIF/subscriptions" + }, + "tax_info": null, + "tax_info_verification": null +} diff --git a/corporate/tests/stripe_fixtures/invoice_plan:Invoice.create.1.json b/corporate/tests/stripe_fixtures/invoice_plan:Invoice.create.1.json new file mode 100644 index 0000000000..ab005bf57b --- /dev/null +++ b/corporate/tests/stripe_fixtures/invoice_plan:Invoice.create.1.json @@ -0,0 +1,92 @@ +{ + "amount_due": 0, + "amount_paid": 0, + "amount_remaining": 0, + "application_fee": null, + "attempt_count": 0, + "attempted": false, + "auto_advance": true, + "billing": "charge_automatically", + "billing_reason": "manual", + "charge": null, + "currency": "usd", + "custom_fields": null, + "customer": "cus_EQUrCdXCP2oqIF", + "date": 1548696360, + "default_source": null, + "description": "", + "discount": null, + "due_date": null, + "ending_balance": null, + "finalized_at": null, + "footer": null, + "hosted_invoice_url": null, + "id": "in_1DxdrwGh0CmXqmnw7x9G2rWd", + "invoice_pdf": null, + "lines": { + "data": [ + { + "amount": 64000, + "currency": "usd", + "description": "Zulip Standard", + "discountable": false, + "id": "ii_1DxdrvGh0CmXqmnwhHM5DcR1", + "invoice_item": "ii_1DxdrvGh0CmXqmnwhHM5DcR1", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1357095845, + "start": 1325473445 + }, + "plan": null, + "proration": false, + "quantity": 8, + "subscription": null, + "type": "invoiceitem" + }, + { + "amount": -64000, + "currency": "usd", + "description": "Payment (Card ending in 4242)", + "discountable": false, + "id": "ii_1DxdrvGh0CmXqmnwg0R6ha8n", + "invoice_item": "ii_1DxdrvGh0CmXqmnwg0R6ha8n", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1548696359, + "start": 1548696359 + }, + "plan": null, + "proration": false, + "quantity": 1, + "subscription": null, + "type": "invoiceitem" + } + ], + "has_more": false, + "object": "list", + "total_count": 2, + "url": "/v1/invoices/in_1DxdrwGh0CmXqmnw7x9G2rWd/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": 1548699960, + "number": "81F7A21-0001", + "object": "invoice", + "paid": false, + "period_end": 1548696360, + "period_start": 1548696360, + "receipt_number": null, + "starting_balance": 0, + "statement_descriptor": "Zulip Standard", + "status": "draft", + "subscription": null, + "subtotal": 0, + "tax": 0, + "tax_percent": null, + "total": 0, + "webhooks_delivered_at": null +} diff --git a/corporate/tests/stripe_fixtures/invoice_plan:Invoice.create.2.json b/corporate/tests/stripe_fixtures/invoice_plan:Invoice.create.2.json new file mode 100644 index 0000000000..8e0138443a --- /dev/null +++ b/corporate/tests/stripe_fixtures/invoice_plan:Invoice.create.2.json @@ -0,0 +1,112 @@ +{ + "amount_due": 96697, + "amount_paid": 0, + "amount_remaining": 96697, + "application_fee": null, + "attempt_count": 0, + "attempted": false, + "auto_advance": true, + "billing": "charge_automatically", + "billing_reason": "manual", + "charge": null, + "currency": "usd", + "custom_fields": null, + "customer": "cus_EQUrCdXCP2oqIF", + "date": 1548696362, + "default_source": null, + "description": "", + "discount": null, + "due_date": null, + "ending_balance": null, + "finalized_at": null, + "footer": null, + "hosted_invoice_url": null, + "id": "in_1DxdryGh0CmXqmnwqSArnOBA", + "invoice_pdf": null, + "lines": { + "data": [ + { + "amount": 7255, + "currency": "usd", + "description": "Additional license (Feb 5, 2013 - Jan 2, 2014)", + "discountable": false, + "id": "ii_1DxdryGh0CmXqmnwXSzdImK3", + "invoice_item": "ii_1DxdryGh0CmXqmnwXSzdImK3", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1388631845, + "start": 1360033445 + }, + "plan": null, + "proration": false, + "quantity": 1, + "subscription": null, + "type": "invoiceitem" + }, + { + "amount": 72000, + "currency": "usd", + "description": "Zulip Standard - renewal", + "discountable": false, + "id": "ii_1DxdrxGh0CmXqmnw183j11lw", + "invoice_item": "ii_1DxdrxGh0CmXqmnw183j11lw", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1388631845, + "start": 1357095845 + }, + "plan": null, + "proration": false, + "quantity": 9, + "subscription": null, + "type": "invoiceitem" + }, + { + "amount": 17442, + "currency": "usd", + "description": "Additional license (Apr 11, 2012 - Jan 2, 2013)", + "discountable": false, + "id": "ii_1DxdrxGh0CmXqmnwSpeA2crG", + "invoice_item": "ii_1DxdrxGh0CmXqmnwSpeA2crG", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1357095845, + "start": 1334113445 + }, + "plan": null, + "proration": false, + "quantity": 3, + "subscription": null, + "type": "invoiceitem" + } + ], + "has_more": false, + "object": "list", + "total_count": 3, + "url": "/v1/invoices/in_1DxdryGh0CmXqmnwqSArnOBA/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": 1548699962, + "number": "81F7A21-0002", + "object": "invoice", + "paid": false, + "period_end": 1548696362, + "period_start": 1548696362, + "receipt_number": null, + "starting_balance": 0, + "statement_descriptor": "Zulip Standard", + "status": "draft", + "subscription": null, + "subtotal": 96697, + "tax": 0, + "tax_percent": null, + "total": 96697, + "webhooks_delivered_at": null +} diff --git a/corporate/tests/stripe_fixtures/invoice_plan:Invoice.finalize_invoice.1.json b/corporate/tests/stripe_fixtures/invoice_plan:Invoice.finalize_invoice.1.json new file mode 100644 index 0000000000..8aff0c37ff --- /dev/null +++ b/corporate/tests/stripe_fixtures/invoice_plan:Invoice.finalize_invoice.1.json @@ -0,0 +1,92 @@ +{ + "amount_due": 0, + "amount_paid": 0, + "amount_remaining": 0, + "application_fee": null, + "attempt_count": 0, + "attempted": true, + "auto_advance": false, + "billing": "charge_automatically", + "billing_reason": "manual", + "charge": null, + "currency": "usd", + "custom_fields": null, + "customer": "cus_EQUrCdXCP2oqIF", + "date": 1548696360, + "default_source": null, + "description": "", + "discount": null, + "due_date": 1551288360, + "ending_balance": 0, + "finalized_at": 1548696360, + "footer": null, + "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_QWWFlNhg4YyUdnWxVhUvuDs8TZ", + "id": "in_1DxdrwGh0CmXqmnw7x9G2rWd", + "invoice_pdf": "https://pay.stripe.com/invoice/invst_QWWFlNhg4YyUdnWxVhUvuDs8TZ/pdf", + "lines": { + "data": [ + { + "amount": 64000, + "currency": "usd", + "description": "Zulip Standard", + "discountable": false, + "id": "ii_1DxdrvGh0CmXqmnwhHM5DcR1", + "invoice_item": "ii_1DxdrvGh0CmXqmnwhHM5DcR1", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1357095845, + "start": 1325473445 + }, + "plan": null, + "proration": false, + "quantity": 8, + "subscription": null, + "type": "invoiceitem" + }, + { + "amount": -64000, + "currency": "usd", + "description": "Payment (Card ending in 4242)", + "discountable": false, + "id": "ii_1DxdrvGh0CmXqmnwg0R6ha8n", + "invoice_item": "ii_1DxdrvGh0CmXqmnwg0R6ha8n", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1548696359, + "start": 1548696359 + }, + "plan": null, + "proration": false, + "quantity": 1, + "subscription": null, + "type": "invoiceitem" + } + ], + "has_more": false, + "object": "list", + "total_count": 2, + "url": "/v1/invoices/in_1DxdrwGh0CmXqmnw7x9G2rWd/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "number": "81F7A21-0001", + "object": "invoice", + "paid": true, + "period_end": 1548696360, + "period_start": 1548696360, + "receipt_number": null, + "starting_balance": 0, + "statement_descriptor": "Zulip Standard", + "status": "paid", + "subscription": null, + "subtotal": 0, + "tax": 0, + "tax_percent": null, + "total": 0, + "webhooks_delivered_at": 1548696360 +} diff --git a/corporate/tests/stripe_fixtures/invoice_plan:Invoice.finalize_invoice.2.json b/corporate/tests/stripe_fixtures/invoice_plan:Invoice.finalize_invoice.2.json new file mode 100644 index 0000000000..869bd01b06 --- /dev/null +++ b/corporate/tests/stripe_fixtures/invoice_plan:Invoice.finalize_invoice.2.json @@ -0,0 +1,112 @@ +{ + "amount_due": 96697, + "amount_paid": 0, + "amount_remaining": 96697, + "application_fee": null, + "attempt_count": 0, + "attempted": false, + "auto_advance": true, + "billing": "charge_automatically", + "billing_reason": "manual", + "charge": null, + "currency": "usd", + "custom_fields": null, + "customer": "cus_EQUrCdXCP2oqIF", + "date": 1548696362, + "default_source": null, + "description": "", + "discount": null, + "due_date": 1551288363, + "ending_balance": 0, + "finalized_at": 1548696363, + "footer": null, + "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_sT02SUZUJrLO3QmdmKIAQwQi9F", + "id": "in_1DxdryGh0CmXqmnwqSArnOBA", + "invoice_pdf": "https://pay.stripe.com/invoice/invst_sT02SUZUJrLO3QmdmKIAQwQi9F/pdf", + "lines": { + "data": [ + { + "amount": 7255, + "currency": "usd", + "description": "Additional license (Feb 5, 2013 - Jan 2, 2014)", + "discountable": false, + "id": "ii_1DxdryGh0CmXqmnwXSzdImK3", + "invoice_item": "ii_1DxdryGh0CmXqmnwXSzdImK3", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1388631845, + "start": 1360033445 + }, + "plan": null, + "proration": false, + "quantity": 1, + "subscription": null, + "type": "invoiceitem" + }, + { + "amount": 72000, + "currency": "usd", + "description": "Zulip Standard - renewal", + "discountable": false, + "id": "ii_1DxdrxGh0CmXqmnw183j11lw", + "invoice_item": "ii_1DxdrxGh0CmXqmnw183j11lw", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1388631845, + "start": 1357095845 + }, + "plan": null, + "proration": false, + "quantity": 9, + "subscription": null, + "type": "invoiceitem" + }, + { + "amount": 17442, + "currency": "usd", + "description": "Additional license (Apr 11, 2012 - Jan 2, 2013)", + "discountable": false, + "id": "ii_1DxdrxGh0CmXqmnwSpeA2crG", + "invoice_item": "ii_1DxdrxGh0CmXqmnwSpeA2crG", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1357095845, + "start": 1334113445 + }, + "plan": null, + "proration": false, + "quantity": 3, + "subscription": null, + "type": "invoiceitem" + } + ], + "has_more": false, + "object": "list", + "total_count": 3, + "url": "/v1/invoices/in_1DxdryGh0CmXqmnwqSArnOBA/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": 1548699962, + "number": "81F7A21-0002", + "object": "invoice", + "paid": false, + "period_end": 1548696362, + "period_start": 1548696362, + "receipt_number": null, + "starting_balance": 0, + "statement_descriptor": "Zulip Standard", + "status": "open", + "subscription": null, + "subtotal": 96697, + "tax": 0, + "tax_percent": null, + "total": 96697, + "webhooks_delivered_at": 1548696362 +} diff --git a/corporate/tests/stripe_fixtures/invoice_plan:Invoice.list.1.json b/corporate/tests/stripe_fixtures/invoice_plan:Invoice.list.1.json new file mode 100644 index 0000000000..510b16b12f --- /dev/null +++ b/corporate/tests/stripe_fixtures/invoice_plan:Invoice.list.1.json @@ -0,0 +1,211 @@ +{ + "data": [ + { + "amount_due": 96697, + "amount_paid": 0, + "amount_remaining": 96697, + "application_fee": null, + "attempt_count": 0, + "attempted": false, + "auto_advance": true, + "billing": "charge_automatically", + "billing_reason": "manual", + "charge": null, + "currency": "usd", + "custom_fields": null, + "customer": "cus_EQUrCdXCP2oqIF", + "date": 1548696362, + "default_source": null, + "description": "", + "discount": null, + "due_date": 1551288363, + "ending_balance": 0, + "finalized_at": 1548696363, + "footer": null, + "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_sT02SUZUJrLO3QmdmKIAQwQi9F", + "id": "in_1DxdryGh0CmXqmnwqSArnOBA", + "invoice_pdf": "https://pay.stripe.com/invoice/invst_sT02SUZUJrLO3QmdmKIAQwQi9F/pdf", + "lines": { + "data": [ + { + "amount": 7255, + "currency": "usd", + "description": "Additional license (Feb 5, 2013 - Jan 2, 2014)", + "discountable": false, + "id": "ii_1DxdryGh0CmXqmnwXSzdImK3", + "invoice_item": "ii_1DxdryGh0CmXqmnwXSzdImK3", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1388631845, + "start": 1360033445 + }, + "plan": null, + "proration": false, + "quantity": 1, + "subscription": null, + "type": "invoiceitem" + }, + { + "amount": 72000, + "currency": "usd", + "description": "Zulip Standard - renewal", + "discountable": false, + "id": "ii_1DxdrxGh0CmXqmnw183j11lw", + "invoice_item": "ii_1DxdrxGh0CmXqmnw183j11lw", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1388631845, + "start": 1357095845 + }, + "plan": null, + "proration": false, + "quantity": 9, + "subscription": null, + "type": "invoiceitem" + }, + { + "amount": 17442, + "currency": "usd", + "description": "Additional license (Apr 11, 2012 - Jan 2, 2013)", + "discountable": false, + "id": "ii_1DxdrxGh0CmXqmnwSpeA2crG", + "invoice_item": "ii_1DxdrxGh0CmXqmnwSpeA2crG", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1357095845, + "start": 1334113445 + }, + "plan": null, + "proration": false, + "quantity": 3, + "subscription": null, + "type": "invoiceitem" + } + ], + "has_more": false, + "object": "list", + "total_count": 3, + "url": "/v1/invoices/in_1DxdryGh0CmXqmnwqSArnOBA/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": 1548699962, + "number": "81F7A21-0002", + "object": "invoice", + "paid": false, + "period_end": 1548696362, + "period_start": 1548696362, + "receipt_number": null, + "starting_balance": 0, + "statement_descriptor": "Zulip Standard", + "status": "open", + "subscription": null, + "subtotal": 96697, + "tax": 0, + "tax_percent": null, + "total": 96697, + "webhooks_delivered_at": 1548696362 + }, + { + "amount_due": 0, + "amount_paid": 0, + "amount_remaining": 0, + "application_fee": null, + "attempt_count": 0, + "attempted": true, + "auto_advance": false, + "billing": "charge_automatically", + "billing_reason": "manual", + "charge": null, + "currency": "usd", + "custom_fields": null, + "customer": "cus_EQUrCdXCP2oqIF", + "date": 1548696360, + "default_source": null, + "description": "", + "discount": null, + "due_date": 1551288360, + "ending_balance": 0, + "finalized_at": 1548696360, + "footer": null, + "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_QWWFlNhg4YyUdnWxVhUvuDs8TZ", + "id": "in_1DxdrwGh0CmXqmnw7x9G2rWd", + "invoice_pdf": "https://pay.stripe.com/invoice/invst_QWWFlNhg4YyUdnWxVhUvuDs8TZ/pdf", + "lines": { + "data": [ + { + "amount": 64000, + "currency": "usd", + "description": "Zulip Standard", + "discountable": false, + "id": "ii_1DxdrvGh0CmXqmnwhHM5DcR1", + "invoice_item": "ii_1DxdrvGh0CmXqmnwhHM5DcR1", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1357095845, + "start": 1325473445 + }, + "plan": null, + "proration": false, + "quantity": 8, + "subscription": null, + "type": "invoiceitem" + }, + { + "amount": -64000, + "currency": "usd", + "description": "Payment (Card ending in 4242)", + "discountable": false, + "id": "ii_1DxdrvGh0CmXqmnwg0R6ha8n", + "invoice_item": "ii_1DxdrvGh0CmXqmnwg0R6ha8n", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1548696359, + "start": 1548696359 + }, + "plan": null, + "proration": false, + "quantity": 1, + "subscription": null, + "type": "invoiceitem" + } + ], + "has_more": false, + "object": "list", + "total_count": 2, + "url": "/v1/invoices/in_1DxdrwGh0CmXqmnw7x9G2rWd/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "number": "81F7A21-0001", + "object": "invoice", + "paid": true, + "period_end": 1548696360, + "period_start": 1548696360, + "receipt_number": null, + "starting_balance": 0, + "statement_descriptor": "Zulip Standard", + "status": "paid", + "subscription": null, + "subtotal": 0, + "tax": 0, + "tax_percent": null, + "total": 0, + "webhooks_delivered_at": 1548696360 + } + ], + "has_more": false, + "object": "list", + "url": "/v1/invoices" +} diff --git a/corporate/tests/stripe_fixtures/invoice_plan:InvoiceItem.create.1.json b/corporate/tests/stripe_fixtures/invoice_plan:InvoiceItem.create.1.json new file mode 100644 index 0000000000..87b9365b50 --- /dev/null +++ b/corporate/tests/stripe_fixtures/invoice_plan:InvoiceItem.create.1.json @@ -0,0 +1,22 @@ +{ + "amount": -64000, + "currency": "usd", + "customer": "cus_EQUrCdXCP2oqIF", + "date": 1548696359, + "description": "Payment (Card ending in 4242)", + "discountable": false, + "id": "ii_1DxdrvGh0CmXqmnwg0R6ha8n", + "invoice": null, + "livemode": false, + "metadata": {}, + "object": "invoiceitem", + "period": { + "end": 1548696359, + "start": 1548696359 + }, + "plan": null, + "proration": false, + "quantity": 1, + "subscription": null, + "unit_amount": -64000 +} diff --git a/corporate/tests/stripe_fixtures/invoice_plan:InvoiceItem.create.2.json b/corporate/tests/stripe_fixtures/invoice_plan:InvoiceItem.create.2.json new file mode 100644 index 0000000000..495f24f7ea --- /dev/null +++ b/corporate/tests/stripe_fixtures/invoice_plan:InvoiceItem.create.2.json @@ -0,0 +1,22 @@ +{ + "amount": 64000, + "currency": "usd", + "customer": "cus_EQUrCdXCP2oqIF", + "date": 1548696359, + "description": "Zulip Standard", + "discountable": false, + "id": "ii_1DxdrvGh0CmXqmnwhHM5DcR1", + "invoice": null, + "livemode": false, + "metadata": {}, + "object": "invoiceitem", + "period": { + "end": 1357095845, + "start": 1325473445 + }, + "plan": null, + "proration": false, + "quantity": 8, + "subscription": null, + "unit_amount": 8000 +} diff --git a/corporate/tests/stripe_fixtures/invoice_plan:InvoiceItem.create.3.json b/corporate/tests/stripe_fixtures/invoice_plan:InvoiceItem.create.3.json new file mode 100644 index 0000000000..ad96238076 --- /dev/null +++ b/corporate/tests/stripe_fixtures/invoice_plan:InvoiceItem.create.3.json @@ -0,0 +1,22 @@ +{ + "amount": 17442, + "currency": "usd", + "customer": "cus_EQUrCdXCP2oqIF", + "date": 1548696361, + "description": "Additional license (Apr 11, 2012 - Jan 2, 2013)", + "discountable": false, + "id": "ii_1DxdrxGh0CmXqmnwSpeA2crG", + "invoice": null, + "livemode": false, + "metadata": {}, + "object": "invoiceitem", + "period": { + "end": 1357095845, + "start": 1334113445 + }, + "plan": null, + "proration": false, + "quantity": 3, + "subscription": null, + "unit_amount": 5814 +} diff --git a/corporate/tests/stripe_fixtures/invoice_plan:InvoiceItem.create.4.json b/corporate/tests/stripe_fixtures/invoice_plan:InvoiceItem.create.4.json new file mode 100644 index 0000000000..3b79b854f6 --- /dev/null +++ b/corporate/tests/stripe_fixtures/invoice_plan:InvoiceItem.create.4.json @@ -0,0 +1,22 @@ +{ + "amount": 72000, + "currency": "usd", + "customer": "cus_EQUrCdXCP2oqIF", + "date": 1548696361, + "description": "Zulip Standard - renewal", + "discountable": false, + "id": "ii_1DxdrxGh0CmXqmnw183j11lw", + "invoice": null, + "livemode": false, + "metadata": {}, + "object": "invoiceitem", + "period": { + "end": 1388631845, + "start": 1357095845 + }, + "plan": null, + "proration": false, + "quantity": 9, + "subscription": null, + "unit_amount": 8000 +} diff --git a/corporate/tests/stripe_fixtures/invoice_plan:InvoiceItem.create.5.json b/corporate/tests/stripe_fixtures/invoice_plan:InvoiceItem.create.5.json new file mode 100644 index 0000000000..bbbb7a344e --- /dev/null +++ b/corporate/tests/stripe_fixtures/invoice_plan:InvoiceItem.create.5.json @@ -0,0 +1,22 @@ +{ + "amount": 7255, + "currency": "usd", + "customer": "cus_EQUrCdXCP2oqIF", + "date": 1548696362, + "description": "Additional license (Feb 5, 2013 - Jan 2, 2014)", + "discountable": false, + "id": "ii_1DxdryGh0CmXqmnwXSzdImK3", + "invoice": null, + "livemode": false, + "metadata": {}, + "object": "invoiceitem", + "period": { + "end": 1388631845, + "start": 1360033445 + }, + "plan": null, + "proration": false, + "quantity": 1, + "subscription": null, + "unit_amount": 7255 +} diff --git a/corporate/tests/stripe_fixtures/invoice_plan:Token.create.1.json b/corporate/tests/stripe_fixtures/invoice_plan:Token.create.1.json new file mode 100644 index 0000000000..b400df63c2 --- /dev/null +++ b/corporate/tests/stripe_fixtures/invoice_plan:Token.create.1.json @@ -0,0 +1,33 @@ +{ + "card": { + "address_city": "Pacific", + "address_country": "United States", + "address_line1": "Under the sea,", + "address_line1_check": "unchecked", + "address_line2": null, + "address_state": null, + "address_zip": "33333", + "address_zip_check": "unchecked", + "brand": "Visa", + "country": "US", + "cvc_check": "unchecked", + "dynamic_last4": null, + "exp_month": 3, + "exp_year": 2033, + "fingerprint": "6dAXT9VZvwro65EK", + "funding": "credit", + "id": "card_1DxdrtGh0CmXqmnw1LvZ0cL4", + "last4": "4242", + "metadata": {}, + "name": "Ada Starr", + "object": "card", + "tokenization_method": null + }, + "client_ip": "107.202.144.213", + "created": 1548696357, + "id": "tok_1DxdrtGh0CmXqmnw94EYeDen", + "livemode": false, + "object": "token", + "type": "card", + "used": false +} diff --git a/corporate/tests/stripe_fixtures/renewal:Charge.create.1.json b/corporate/tests/stripe_fixtures/renewal:Charge.create.1.json new file mode 100644 index 0000000000..46de6dfcd2 --- /dev/null +++ b/corporate/tests/stripe_fixtures/renewal:Charge.create.1.json @@ -0,0 +1,78 @@ +{ + "amount": 64000, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": "txn_1DxdeSGh0CmXqmnw9I7ziL6D", + "captured": true, + "created": 1548695524, + "currency": "usd", + "customer": "cus_EQUd48LphR5ahk", + "description": "Upgrade to Zulip Standard, $80.0 x 8", + "destination": null, + "dispute": null, + "failure_code": null, + "failure_message": null, + "fraud_details": {}, + "id": "ch_1DxdeSGh0CmXqmnwsw80Rn8n", + "invoice": null, + "livemode": false, + "metadata": {}, + "object": "charge", + "on_behalf_of": null, + "order": null, + "outcome": { + "network_status": "approved_by_network", + "reason": null, + "risk_level": "normal", + "risk_score": 46, + "seller_message": "Payment complete.", + "type": "authorized" + }, + "paid": true, + "payment_intent": null, + "receipt_email": "hamlet@zulip.com", + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/acct_1BWYgHGh0CmXqmnw/ch_1DxdeSGh0CmXqmnwsw80Rn8n/rcpt_EQUda9O5I5cYpJL0yxiRbxnPbWZ415D", + "refunded": false, + "refunds": { + "data": [], + "has_more": false, + "object": "list", + "total_count": 0, + "url": "/v1/charges/ch_1DxdeSGh0CmXqmnwsw80Rn8n/refunds" + }, + "review": null, + "shipping": null, + "source": { + "address_city": "Pacific", + "address_country": "United States", + "address_line1": "Under the sea,", + "address_line1_check": "pass", + "address_line2": null, + "address_state": null, + "address_zip": "33333", + "address_zip_check": "pass", + "brand": "Visa", + "country": "US", + "customer": "cus_EQUd48LphR5ahk", + "cvc_check": "pass", + "dynamic_last4": null, + "exp_month": 3, + "exp_year": 2033, + "fingerprint": "6dAXT9VZvwro65EK", + "funding": "credit", + "id": "card_1DxdeRGh0CmXqmnwu3BibWtM", + "last4": "4242", + "metadata": {}, + "name": "Ada Starr", + "object": "card", + "tokenization_method": null + }, + "source_transfer": null, + "statement_descriptor": "Zulip Standard", + "status": "succeeded", + "transfer_data": null, + "transfer_group": null +} diff --git a/corporate/tests/stripe_fixtures/renewal:Customer.create.1.json b/corporate/tests/stripe_fixtures/renewal:Customer.create.1.json new file mode 100644 index 0000000000..c68160548d --- /dev/null +++ b/corporate/tests/stripe_fixtures/renewal:Customer.create.1.json @@ -0,0 +1,65 @@ +{ + "account_balance": 0, + "created": 1548695523, + "currency": null, + "default_source": "card_1DxdeRGh0CmXqmnwu3BibWtM", + "delinquent": false, + "description": "zulip (Zulip Dev)", + "discount": null, + "email": "hamlet@zulip.com", + "id": "cus_EQUd48LphR5ahk", + "invoice_prefix": "9F53235", + "invoice_settings": { + "custom_fields": null, + "footer": null + }, + "livemode": false, + "metadata": { + "realm_id": "1", + "realm_str": "zulip" + }, + "object": "customer", + "shipping": null, + "sources": { + "data": [ + { + "address_city": "Pacific", + "address_country": "United States", + "address_line1": "Under the sea,", + "address_line1_check": "pass", + "address_line2": null, + "address_state": null, + "address_zip": "33333", + "address_zip_check": "pass", + "brand": "Visa", + "country": "US", + "customer": "cus_EQUd48LphR5ahk", + "cvc_check": "pass", + "dynamic_last4": null, + "exp_month": 3, + "exp_year": 2033, + "fingerprint": "6dAXT9VZvwro65EK", + "funding": "credit", + "id": "card_1DxdeRGh0CmXqmnwu3BibWtM", + "last4": "4242", + "metadata": {}, + "name": "Ada Starr", + "object": "card", + "tokenization_method": null + } + ], + "has_more": false, + "object": "list", + "total_count": 1, + "url": "/v1/customers/cus_EQUd48LphR5ahk/sources" + }, + "subscriptions": { + "data": [], + "has_more": false, + "object": "list", + "total_count": 0, + "url": "/v1/customers/cus_EQUd48LphR5ahk/subscriptions" + }, + "tax_info": null, + "tax_info_verification": null +} diff --git a/corporate/tests/stripe_fixtures/renewal:Invoice.create.1.json b/corporate/tests/stripe_fixtures/renewal:Invoice.create.1.json new file mode 100644 index 0000000000..fac463effd --- /dev/null +++ b/corporate/tests/stripe_fixtures/renewal:Invoice.create.1.json @@ -0,0 +1,92 @@ +{ + "amount_due": 0, + "amount_paid": 0, + "amount_remaining": 0, + "application_fee": null, + "attempt_count": 0, + "attempted": false, + "auto_advance": true, + "billing": "charge_automatically", + "billing_reason": "manual", + "charge": null, + "currency": "usd", + "custom_fields": null, + "customer": "cus_EQUd48LphR5ahk", + "date": 1548695526, + "default_source": null, + "description": "", + "discount": null, + "due_date": null, + "ending_balance": null, + "finalized_at": null, + "footer": null, + "hosted_invoice_url": null, + "id": "in_1DxdeUGh0CmXqmnwoowtAcQW", + "invoice_pdf": null, + "lines": { + "data": [ + { + "amount": 64000, + "currency": "usd", + "description": "Zulip Standard", + "discountable": false, + "id": "ii_1DxdeTGh0CmXqmnw7MfZoBVx", + "invoice_item": "ii_1DxdeTGh0CmXqmnw7MfZoBVx", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1357095845, + "start": 1325473445 + }, + "plan": null, + "proration": false, + "quantity": 8, + "subscription": null, + "type": "invoiceitem" + }, + { + "amount": -64000, + "currency": "usd", + "description": "Payment (Card ending in 4242)", + "discountable": false, + "id": "ii_1DxdeTGh0CmXqmnwDYYlalDE", + "invoice_item": "ii_1DxdeTGh0CmXqmnwDYYlalDE", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1548695525, + "start": 1548695525 + }, + "plan": null, + "proration": false, + "quantity": 1, + "subscription": null, + "type": "invoiceitem" + } + ], + "has_more": false, + "object": "list", + "total_count": 2, + "url": "/v1/invoices/in_1DxdeUGh0CmXqmnwoowtAcQW/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": 1548699126, + "number": "9F53235-0001", + "object": "invoice", + "paid": false, + "period_end": 1548695526, + "period_start": 1548695526, + "receipt_number": null, + "starting_balance": 0, + "statement_descriptor": "Zulip Standard", + "status": "draft", + "subscription": null, + "subtotal": 0, + "tax": 0, + "tax_percent": null, + "total": 0, + "webhooks_delivered_at": null +} diff --git a/corporate/tests/stripe_fixtures/renewal:Invoice.finalize_invoice.1.json b/corporate/tests/stripe_fixtures/renewal:Invoice.finalize_invoice.1.json new file mode 100644 index 0000000000..2dd3de8cde --- /dev/null +++ b/corporate/tests/stripe_fixtures/renewal:Invoice.finalize_invoice.1.json @@ -0,0 +1,92 @@ +{ + "amount_due": 0, + "amount_paid": 0, + "amount_remaining": 0, + "application_fee": null, + "attempt_count": 0, + "attempted": true, + "auto_advance": false, + "billing": "charge_automatically", + "billing_reason": "manual", + "charge": null, + "currency": "usd", + "custom_fields": null, + "customer": "cus_EQUd48LphR5ahk", + "date": 1548695526, + "default_source": null, + "description": "", + "discount": null, + "due_date": 1551287527, + "ending_balance": 0, + "finalized_at": 1548695527, + "footer": null, + "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_8N3Zw202aBfkZSHzyjUVcv9vrE", + "id": "in_1DxdeUGh0CmXqmnwoowtAcQW", + "invoice_pdf": "https://pay.stripe.com/invoice/invst_8N3Zw202aBfkZSHzyjUVcv9vrE/pdf", + "lines": { + "data": [ + { + "amount": 64000, + "currency": "usd", + "description": "Zulip Standard", + "discountable": false, + "id": "ii_1DxdeTGh0CmXqmnw7MfZoBVx", + "invoice_item": "ii_1DxdeTGh0CmXqmnw7MfZoBVx", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1357095845, + "start": 1325473445 + }, + "plan": null, + "proration": false, + "quantity": 8, + "subscription": null, + "type": "invoiceitem" + }, + { + "amount": -64000, + "currency": "usd", + "description": "Payment (Card ending in 4242)", + "discountable": false, + "id": "ii_1DxdeTGh0CmXqmnwDYYlalDE", + "invoice_item": "ii_1DxdeTGh0CmXqmnwDYYlalDE", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1548695525, + "start": 1548695525 + }, + "plan": null, + "proration": false, + "quantity": 1, + "subscription": null, + "type": "invoiceitem" + } + ], + "has_more": false, + "object": "list", + "total_count": 2, + "url": "/v1/invoices/in_1DxdeUGh0CmXqmnwoowtAcQW/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "number": "9F53235-0001", + "object": "invoice", + "paid": true, + "period_end": 1548695526, + "period_start": 1548695526, + "receipt_number": null, + "starting_balance": 0, + "statement_descriptor": "Zulip Standard", + "status": "paid", + "subscription": null, + "subtotal": 0, + "tax": 0, + "tax_percent": null, + "total": 0, + "webhooks_delivered_at": 1548695526 +} diff --git a/corporate/tests/stripe_fixtures/renewal:InvoiceItem.create.1.json b/corporate/tests/stripe_fixtures/renewal:InvoiceItem.create.1.json new file mode 100644 index 0000000000..6e9e4d62d3 --- /dev/null +++ b/corporate/tests/stripe_fixtures/renewal:InvoiceItem.create.1.json @@ -0,0 +1,22 @@ +{ + "amount": -64000, + "currency": "usd", + "customer": "cus_EQUd48LphR5ahk", + "date": 1548695525, + "description": "Payment (Card ending in 4242)", + "discountable": false, + "id": "ii_1DxdeTGh0CmXqmnwDYYlalDE", + "invoice": null, + "livemode": false, + "metadata": {}, + "object": "invoiceitem", + "period": { + "end": 1548695525, + "start": 1548695525 + }, + "plan": null, + "proration": false, + "quantity": 1, + "subscription": null, + "unit_amount": -64000 +} diff --git a/corporate/tests/stripe_fixtures/renewal:InvoiceItem.create.2.json b/corporate/tests/stripe_fixtures/renewal:InvoiceItem.create.2.json new file mode 100644 index 0000000000..356f9c116f --- /dev/null +++ b/corporate/tests/stripe_fixtures/renewal:InvoiceItem.create.2.json @@ -0,0 +1,22 @@ +{ + "amount": 64000, + "currency": "usd", + "customer": "cus_EQUd48LphR5ahk", + "date": 1548695525, + "description": "Zulip Standard", + "discountable": false, + "id": "ii_1DxdeTGh0CmXqmnw7MfZoBVx", + "invoice": null, + "livemode": false, + "metadata": {}, + "object": "invoiceitem", + "period": { + "end": 1357095845, + "start": 1325473445 + }, + "plan": null, + "proration": false, + "quantity": 8, + "subscription": null, + "unit_amount": 8000 +} diff --git a/corporate/tests/stripe_fixtures/renewal:InvoiceItem.create.3.json b/corporate/tests/stripe_fixtures/renewal:InvoiceItem.create.3.json new file mode 100644 index 0000000000..07a2c4e918 --- /dev/null +++ b/corporate/tests/stripe_fixtures/renewal:InvoiceItem.create.3.json @@ -0,0 +1,22 @@ +{ + "amount": 17442, + "currency": "usd", + "customer": "cus_EQUd48LphR5ahk", + "date": 1548695527, + "description": "Additional license (Apr 11, 2012 - Jan 2, 2013)", + "discountable": false, + "id": "ii_1DxdeVGh0CmXqmnwf6cLlEA7", + "invoice": null, + "livemode": false, + "metadata": {}, + "object": "invoiceitem", + "period": { + "end": 1357095845, + "start": 1334113445 + }, + "plan": null, + "proration": false, + "quantity": 3, + "subscription": null, + "unit_amount": 5814 +} diff --git a/corporate/tests/stripe_fixtures/renewal:Token.create.1.json b/corporate/tests/stripe_fixtures/renewal:Token.create.1.json new file mode 100644 index 0000000000..8bcc7b6dc7 --- /dev/null +++ b/corporate/tests/stripe_fixtures/renewal:Token.create.1.json @@ -0,0 +1,33 @@ +{ + "card": { + "address_city": "Pacific", + "address_country": "United States", + "address_line1": "Under the sea,", + "address_line1_check": "unchecked", + "address_line2": null, + "address_state": null, + "address_zip": "33333", + "address_zip_check": "unchecked", + "brand": "Visa", + "country": "US", + "cvc_check": "unchecked", + "dynamic_last4": null, + "exp_month": 3, + "exp_year": 2033, + "fingerprint": "6dAXT9VZvwro65EK", + "funding": "credit", + "id": "card_1DxdeRGh0CmXqmnwu3BibWtM", + "last4": "4242", + "metadata": {}, + "name": "Ada Starr", + "object": "card", + "tokenization_method": null + }, + "client_ip": "107.202.144.213", + "created": 1548695523, + "id": "tok_1DxdeRGh0CmXqmnw5X6CeYnC", + "livemode": false, + "object": "token", + "type": "card", + "used": false +} diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index 1514911a70..a83b1e5edc 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -30,7 +30,8 @@ from corporate.lib.stripe import catch_stripe_errors, attach_discount_to_realm, add_months, next_month, next_renewal_date, renewal_amount, \ compute_plan_parameters, update_or_create_stripe_customer, \ process_initial_upgrade, add_plan_renewal_to_license_ledger_if_needed, \ - update_license_ledger_if_needed, update_license_ledger_for_automanaged_plan + update_license_ledger_if_needed, update_license_ledger_for_automanaged_plan, \ + invoice_plan, invoice_plans_as_needed from corporate.models import Customer, CustomerPlan, LicenseLedger from corporate.views import payment_method_string import corporate.urls @@ -1027,3 +1028,127 @@ class LicenseLedgerTest(StripeTestCase): (False, self.seat_count + 1, self.seat_count), (False, self.seat_count + 1, self.seat_count + 1), (False, self.seat_count + 1, self.seat_count + 1)]) + +class InvoiceTest(StripeTestCase): + def test_invoicing_status_is_started(self) -> None: + self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, 'token') + plan = CustomerPlan.objects.first() + plan.invoicing_status = CustomerPlan.STARTED + plan.save(update_fields=['invoicing_status']) + with self.assertRaises(NotImplementedError): + invoice_plan(CustomerPlan.objects.first(), self.now) + + @mock_stripe() + def test_invoice_plan(self, *mocks: Mock) -> None: + user = self.example_user("hamlet") + self.login(user.email) + with patch('corporate.lib.stripe.timezone_now', return_value=self.now): + self.upgrade() + # Increase + with patch('corporate.lib.stripe.get_seat_count', return_value=self.seat_count + 3): + update_license_ledger_if_needed(get_realm('zulip'), self.now + timedelta(days=100)) + # Decrease + with patch('corporate.lib.stripe.get_seat_count', return_value=self.seat_count): + update_license_ledger_if_needed(get_realm('zulip'), self.now + timedelta(days=200)) + # Increase, but not past high watermark + with patch('corporate.lib.stripe.get_seat_count', return_value=self.seat_count + 1): + update_license_ledger_if_needed(get_realm('zulip'), self.now + timedelta(days=300)) + # Increase, but after renewal date, and below last year's high watermark + with patch('corporate.lib.stripe.get_seat_count', return_value=self.seat_count + 2): + update_license_ledger_if_needed(get_realm('zulip'), self.now + timedelta(days=400)) + # Increase, but after event_time + with patch('corporate.lib.stripe.get_seat_count', return_value=self.seat_count + 3): + update_license_ledger_if_needed(get_realm('zulip'), self.now + timedelta(days=500)) + plan = CustomerPlan.objects.first() + invoice_plan(plan, self.now + timedelta(days=400)) + + stripe_invoices = [invoice for invoice in stripe.Invoice.list( + customer=plan.customer.stripe_customer_id)] + self.assertEqual(len(stripe_invoices), 2) + self.assertIsNotNone(stripe_invoices[0].finalized_at) + stripe_line_items = [item for item in stripe_invoices[0].lines] + self.assertEqual(len(stripe_line_items), 3) + line_item_params = { + 'amount': int(8000 * (1 - ((400-366) / 365)) + .5), + 'description': 'Additional license (Feb 5, 2013 - Jan 2, 2014)', + 'discountable': False, + 'period': { + 'start': datetime_to_timestamp(self.now + timedelta(days=400)), + 'end': datetime_to_timestamp(self.now + timedelta(days=2*365 + 1))}, + 'quantity': 1} + for key, value in line_item_params.items(): + self.assertEqual(stripe_line_items[0].get(key), value) + line_item_params = { + 'amount': 8000 * (self.seat_count + 1), + 'description': 'Zulip Standard - renewal', + 'discountable': False, + 'period': { + 'start': datetime_to_timestamp(self.now + timedelta(days=366)), + 'end': datetime_to_timestamp(self.now + timedelta(days=2*365 + 1))}, + 'quantity': (self.seat_count + 1)} + for key, value in line_item_params.items(): + self.assertEqual(stripe_line_items[1].get(key), value) + line_item_params = { + 'amount': 3 * int(8000 * (366-100) / 366 + .5), + 'description': 'Additional license (Apr 11, 2012 - Jan 2, 2013)', + 'discountable': False, + 'period': { + 'start': datetime_to_timestamp(self.now + timedelta(days=100)), + 'end': datetime_to_timestamp(self.now + timedelta(days=366))}, + 'quantity': 3} + for key, value in line_item_params.items(): + self.assertEqual(stripe_line_items[2].get(key), value) + + @mock_stripe() + def test_fixed_price_plans(self, *mocks: Mock) -> None: + # Also tests charge_automatically=False + user = self.example_user("hamlet") + self.login(user.email) + with patch('corporate.lib.stripe.timezone_now', return_value=self.now): + self.upgrade(invoice=True) + plan = CustomerPlan.objects.first() + plan.fixed_price = 100 + plan.price_per_license = 0 + plan.save(update_fields=['fixed_price', 'price_per_license']) + invoice_plan(plan, self.next_year) + stripe_invoices = [invoice for invoice in stripe.Invoice.list( + customer=plan.customer.stripe_customer_id)] + self.assertEqual(len(stripe_invoices), 2) + self.assertEqual(stripe_invoices[0].billing, 'send_invoice') + stripe_line_items = [item for item in stripe_invoices[0].lines] + self.assertEqual(len(stripe_line_items), 1) + line_item_params = { + 'amount': 100, + 'description': 'Zulip Standard - renewal', + 'discountable': False, + 'period': { + 'start': datetime_to_timestamp(self.next_year), + 'end': datetime_to_timestamp(self.next_year + timedelta(days=365))}, + 'quantity': 1} + for key, value in line_item_params.items(): + self.assertEqual(stripe_line_items[0].get(key), value) + + def test_no_invoice_needed(self) -> None: + with patch('corporate.lib.stripe.timezone_now', return_value=self.now): + self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, 'token') + plan = CustomerPlan.objects.first() + self.assertEqual(plan.next_invoice_date, self.next_month) + # Test this doesn't make any calls to stripe.Invoice or stripe.InvoiceItem + invoice_plan(plan, self.next_month) + plan = CustomerPlan.objects.first() + # Test that we still update next_invoice_date + self.assertEqual(plan.next_invoice_date, self.next_month + timedelta(days=29)) + + def test_invoice_plans_as_needed(self) -> None: + with patch('corporate.lib.stripe.timezone_now', return_value=self.now): + self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, 'token') + plan = CustomerPlan.objects.first() + self.assertEqual(plan.next_invoice_date, self.next_month) + # Test nothing needed to be done + with patch('corporate.lib.stripe.invoice_plan') as mocked: + invoice_plans_as_needed(self.next_month - timedelta(days=1)) + mocked.assert_not_called() + # Test something needing to be done + invoice_plans_as_needed(self.next_month) + plan = CustomerPlan.objects.first() + self.assertEqual(plan.next_invoice_date, self.next_month + timedelta(days=29)) diff --git a/stubs/stripe/__init__.pyi b/stubs/stripe/__init__.pyi index a437ed81f9..1368cc4f04 100644 --- a/stubs/stripe/__init__.pyi +++ b/stubs/stripe/__init__.pyi @@ -173,7 +173,7 @@ class InvoiceItem: @staticmethod def create(amount: int=..., currency: str=..., customer: str=..., description: str=..., discountable: bool=..., period: Dict[str, int]=..., quantity: int=..., - unit_amount: int=...) -> InvoiceItem: + unit_amount: int=..., idempotency_key: str=...) -> InvoiceItem: ... @staticmethod