~sircmpwn/meta.sr.ht

ref: 72548bd7545f78670878667674cc7645835a17bd meta.sr.ht/metasrht/blueprints/billing.py -rw-r--r-- 9.5 KiB
72548bd7Drew DeVault API: Updates per core-go auth changes 1 year, 1 month ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
import stripe
from datetime import datetime, timedelta
from flask import Blueprint, render_template, request, redirect
from flask import url_for, abort, Response
from jinja2 import escape
from metasrht.audit import audit_log
from metasrht.billing import charge_user
from metasrht.types import User, UserType, PaymentInterval, Invoice
from metasrht.webhooks import deliver_profile_update
from sqlalchemy import and_
from srht.config import cfg
from srht.database import db
from srht.flask import session
from srht.oauth import current_user, loginrequired, freshen_user
from srht.validation import Validation
from weasyprint import HTML, CSS

billing = Blueprint('billing', __name__)
onboarding_redirect = cfg("meta.sr.ht::settings", "onboarding-redirect")

@billing.route("/billing")
@loginrequired
def billing_GET():
    message = session.get("message")
    if message:
        del session["message"]
    customer = None
    if current_user.stripe_customer:
        customer = stripe.Customer.retrieve(current_user.stripe_customer)
    total_users = (User.query
            .filter(User.user_type != UserType.unconfirmed)
            .filter(User.user_type != UserType.suspended)).count()
    total_paid = (User.query
            .filter(User.payment_cents != 0)
            .filter(User.user_type == UserType.active_paying)).count()
    invoices = (Invoice.query
            .filter(Invoice.user_id == current_user.id)
            .order_by(Invoice.created.desc())).all()
    return render_template("billing.html", message=message, customer=customer,
            total_users=total_users, total_paid=total_paid,
            paid_pct="{:.2f}".format(total_paid / total_users * 100),
            invoices=invoices)

@billing.route("/billing/initial")
@loginrequired
def billing_initial_GET():
    total_users = (User.query
            .filter(User.user_type != UserType.unconfirmed)
            .filter(User.user_type != UserType.suspended)).count()
    total_paid = (User.query
            .filter(User.payment_cents != 0)
            .filter(User.user_type == UserType.active_paying)).count()
    return_to = request.args.get("return_to")
    if return_to:
        session["return_to"] = return_to
    return render_template("billing-initial.html",
            total_users=total_users, total_paid=total_paid,
            paid_pct="{:.2f}".format(total_paid / total_users * 100))

@billing.route("/billing/initial", methods=["POST"])
@loginrequired
def billing_initial_POST():
    valid = Validation(request)
    amount = valid.require("amount")
    amount = int(amount)
    plan = valid.require("plan")
    valid.expect(not amount or amount > 0, "Expected amount >0")
    if not valid.ok:
        return "Invalid form submission", 400
    current_user.payment_cents = amount
    db.session.commit()
    if current_user.stripe_customer:
        return redirect(url_for("billing.billing_chperiod_GET"))
    return redirect(url_for("billing.new_payment_GET"))

@billing.route("/billing/change-period")
@loginrequired
def billing_chperiod_GET():
    if not current_user.stripe_customer:
        return redirect(url_for("billing.new_payment_GET"))
    return render_template("billing-change-period.html")

@billing.route("/billing/change-period", methods=["POST"])
def billing_chperiod_POST():
    if not current_user.stripe_customer:
        return redirect(url_for("billing.new_payment_GET"))
    valid = Validation(request)
    term = valid.require("term")
    audit_log("billing", "Payment term changed")
    current_user.payment_interval = PaymentInterval(term)
    success, details = charge_user(current_user)
    db.session.commit()
    freshen_user()
    deliver_profile_update(current_user)

    return_to = session.pop("return_to", None)
    if return_to:
        return redirect(return_to)
    session["message"] = "Your subscription has been updated."
    return redirect(url_for("billing.billing_GET"))

@billing.route("/billing/new-payment")
@loginrequired
def new_payment_GET():
    if not current_user.payment_cents:
        return redirect(url_for("billing.billing_initial_GET"))
    return render_template("new-payment.html",
            amount=current_user.payment_cents)

@billing.route("/billing/new-payment", methods=["POST"])
@loginrequired
def new_payment_POST():
    valid = Validation(request)
    term = valid.require("term")
    token = valid.require("stripe-token")
    if not valid.ok:
        return "Invalid form submission", 400
    if not current_user.stripe_customer:
        new_customer = True
        try:
            customer = stripe.Customer.create(
                    description="~" + current_user.username,
                    email=current_user.email,
                    card=token)
            current_user.stripe_customer = customer.id
            current_user.payment_due = datetime.utcnow() + timedelta(minutes=-5)
        except stripe.error.CardError as e:
            details = e.json_body["error"]["message"]
            return render_template("new-payment.html",
                    amount=current_user.payment_cents, error=details)
    else:
        new_customer = False
        if current_user.user_type != UserType.active_paying:
            current_user.payment_due = datetime.utcnow() + timedelta(minutes=-5)
        try:
            customer = stripe.Customer.retrieve(current_user.stripe_customer)
            source = customer.sources.create(source=token)
            customer.default_source = source.stripe_id
            customer.save()
        except stripe.error.CardError as e:
            details = e.json_body["error"]["message"]
            return render_template("new-payment.html",
                    amount=current_user.payment_cents, error=details)
    audit_log("billing", "New payment method handed")
    current_user.payment_interval = PaymentInterval(term)
    success, details = charge_user(current_user)
    if not success:
        return render_template("new-payment.html",
                amount=current_user.payment_cents, error=details)
    db.session.commit()
    freshen_user()
    deliver_profile_update(current_user)

    return_to = session.pop("return_to", None)
    if return_to:
        return redirect(return_to)
    if new_customer:
        return redirect(url_for("billing.billing_complete"))
    session["message"] = "Your payment method was updated."
    return redirect(url_for("billing.billing_GET"))

@billing.route("/billing/remove-source/<source_id>", methods=["POST"])
@loginrequired
def payment_source_remove(source_id):
    try:
        stripe.Customer.delete_source(
                current_user.stripe_customer,
                source_id)
    except stripe.error.StripeError:
        abort(404)
    session["message"] = "Your payment method was removed successfully."
    return redirect(url_for("billing.billing_GET"))

@billing.route("/billing/set-default-source/<source_id>", methods=["POST"])
@loginrequired
def payment_source_make_default(source_id):
    try:
        stripe.Customer.modify(
                current_user.stripe_customer,
                default_source=source_id)
    except stripe.error.StripeError as ex:
        print(ex)
        abort(404)
    session["message"] = "Your payment method was updated successfully."
    return redirect(url_for("billing.billing_GET"))

@billing.route("/billing/complete")
@loginrequired
def billing_complete():
    return render_template("billing-complete.html",
            onboarding_redirect=onboarding_redirect)

@billing.route("/billing/cancel", methods=["POST"])
@loginrequired
def cancel_POST():
    current_user.payment_cents = 0
    db.session.commit()
    freshen_user()
    deliver_profile_update(current_user)
    audit_log("billing", "Plan cancelled (will not renew)")
    return redirect(url_for("billing.billing_GET"))

@billing.route("/billing/invoice/<int:invoice_id>")
@loginrequired
def invoice_GET(invoice_id):
    invoice = Invoice.query.filter(Invoice.id == invoice_id).one_or_none()
    if not invoice:
        abort(404)
    if (invoice.user_id != current_user.id 
            and current_user.user_type != UserType.admin):
        abort(401)
    return render_template("billing-invoice.html", invoice=invoice)

@billing.route("/billing/invoice/<int:invoice_id>", methods=["POST"])
@loginrequired
def invoice_POST(invoice_id):
    invoice = Invoice.query.filter(Invoice.id == invoice_id).one_or_none()
    if not invoice:
        abort(404)
    if (invoice.user_id != current_user.id 
            and current_user.user_type != UserType.admin):
        abort(401)
    valid = Validation(request)
    bill_to = valid.optional("address-to")
    if not bill_to:
        bill_to = "~" + invoice.user.username
    bill_from = [l for l in [
        cfg("meta.sr.ht::billing", "address-line1", default=None),
        cfg("meta.sr.ht::billing", "address-line2", default=None),
        cfg("meta.sr.ht::billing", "address-line3", default=None),
        cfg("meta.sr.ht::billing", "address-line4", default=None)
    ] if l]

    # Split bill_to to first row (rendered as heading) and others
    [bill_from_head, *bill_from_tail] = bill_from or [None]

    html = render_template("billing-invoice-pdf.html",
        invoice=invoice,
        amount=f"${invoice.cents / 100:.2f}",
        source=invoice.source,
        created=invoice.created.strftime("%Y-%m-%d"),
        valid_thru=invoice.valid_thru.strftime("%Y-%m-%d"),
        bill_to=bill_to,
        bill_from_head=bill_from_head,
        bill_from_tail=bill_from_tail)

    pdf = HTML(string=html).write_pdf()

    filename = f"invoice_{invoice.id}.pdf"
    headers = [('Content-Disposition', f'attachment; filename="{filename}"')]
    return Response(pdf, mimetype="application/pdf", headers=headers)