Today I am working on fokais.com, trying to get to a point where I can launch by workig through stripe integrations. This is my first time using stripe, so there has been quite a bit to learn, and I am probably building in more than I need to before launching, but I am learning, and not in a rush to launch.
I am building the fokais backent in python primarilyt with fastapi and sqlmodel on sqlite. My billing integration is going to be all Stripe.
Stripe Subscription Cancellations Docs #
Here is a link to the stripe docs for your refrence, especially if you want to see how to cancel subscriptions in other languages. They include code samples for many popular languages.
User Model #
This is the part of the user model that includes the cancel and reactivate methods. It pretty much follows the stripe guide.
class UserBase(SQLModel, table=False): # type: ignore[call-arg]
username: str = Field(unique=True)
full_name: str
email: str
email_verified: bool = False
disabled: bool = False
signup_date: Optional[datetime] = Field(default_factory=datetime.utcnow)
stripe_customer_id: Optional[str]
def cancel_subscription(self):
for subscription in self.active_subscriptions:
stripe.Subscription.modify(
subscription.id,
cancel_at_period_end=True,
)
self.refresh()
def reactivate_subscription(self):
for subscription in self.active_subscriptions:
stripe.Subscription.modify(
subscription.id,
cancel_at_period_end=False,
)
self.refresh()
Cancellations api #
Here is the cancellations api. I created an are you sure form that I can link
to from the accounts page with a normal anchor tag. Note that I am doing a
POST request to do the cancellation from a form. I want this to work for any
user whether there is js or not. This is an operation that will change the
users data, and I want to make sure that it avoids all browser and cdn caching.
As a scrappy startup we are running light on infrastructure and are caching
hard at the CDN to avoid excessive server hits.
Note
I am doing a `POST` request to do the cancellation from a form.
@pricing_router.get("/cancel")
@pricing_router.get("/cancel/")
def get_cancel(
request: Request,
current_user: Annotated[User, Depends(get_current_user_if_logged_in)],
):
return config.templates.TemplateResponse(
"cancel.html",
{
"request": request,
"prices": products.prices,
"products": products.products,
"current_user": current_user,
},
)
@pricing_router.post("/cancel")
@pricing_router.post("/cancel/")
def post_cancel(
request: Request,
current_user: Annotated[User, Depends(get_current_user_if_logged_in)],
):
current_user.cancel_subscription()
return HTMLResponse('<p id="cancel" hx-swap-oob="outerHTML">Your Subscription has been Cancelled</p>')
Reactivations #
Reactivating accounts looks just about the same as cancelling, only flippng True to False.
@pricing_router.get("/reactivate")
@pricing_router.get("/reactivate/")
def get_reactivate(
request: Request,
current_user: Annotated[User, Depends(get_current_user_if_logged_in)],
):
return config.templates.TemplateResponse(
"reactivate.html",
{
"request": request,
"prices": products.prices,
"products": products.products,
"current_user": current_user,
},
)
@pricing_router.post("/reactivate")
@pricing_router.post("/reactivate/")
def post_reactivate(
request: Request,
current_user: Annotated[User, Depends(get_current_user_if_logged_in)],
):
current_user.reactivate_subscription()
return HTMLResponse('<p id="reactivate" hx-swap-oob="outerHTML">Your Subscription has been reactivated</p>')
Full User Model #
This is the full user model, completely subject to change in the future, but it includes the cancel and reactivate methods.
class UserBase(SQLModel, table=False): # type: ignore[call-arg]
username: str = Field(unique=True)
full_name: str
email: str
email_verified: bool = False
disabled: bool = False
signup_date: Optional[datetime] = Field(default_factory=datetime.utcnow)
stripe_customer_id: Optional[str]
@property
def session(self):
return next(get_session())
@classmethod
def get_by_id(cls, id):
return next(get_session()).get(cls, id)
def refresh(self):
cache.set(f"active_subscriptions_{self.id}", None, 3600)
cache.set(f"active_products_{self.id}", None, 3600)
def get_checkout_sessions(self):
return [
stripe.checkout.Session.retrieve(s.stripe_checkout_session_id)
for s in self.session.exec(select(CheckoutSession).where(CheckoutSession.user_id == self.id)).all()
if s.stripe_checkout_session_id is not None
]
def get_active_subscriptions(self):
subscriptions = [
s.subscription
for s in [
stripe.checkout.Session.retrieve(s.stripe_checkout_session_id)
for s in self.session.exec(select(CheckoutSession).where(CheckoutSession.user_id == self.id)).all()
if s.stripe_checkout_session_id is not None
]
if s.status == "complete"
]
active_subscriptions = [stripe.Subscription.retrieve(subscription) for subscription in subscriptions]
return active_subscriptions
def has_active_subscription(self):
return len(self.active_subscriptions) > 0
@property
def active_subscriptions(self):
active_subscriptions = cache.get(f"active_subscriptions_{self.id}")
if active_subscriptions is not None:
return active_subscriptions
active_subscriptions = self.get_active_subscriptions()
cache.set(f"active_subscriptions_{self.id}", active_subscriptions, 3600)
return active_subscriptions
@property
def active_plans(self):
subscriptions = self.active_subscriptions
plans = [subscription.plan for subscription in subscriptions]
return plans
@property
def subscription_to_plan(self):
subscriptions = self.active_subscriptions
plans = {subscription.id: subscription.plan.id for subscription in subscriptions}
return plans
@property
def plan_to_subscription(self):
plans = {v: k for k, v in self.subscription_to_plan.items()}
return plans
def get_active_products(self):
plans = self.active_plans
products = [stripe.Product.retrieve(plan.product) for plan in plans]
return products
@property
def plan_to_product(self):
plans = self.active_plans
products = {plan.id: stripe.Product.retrieve(plan.product).id for plan in plans}
return products
@property
def prodct_to_plan(self):
plans = self.active_plans
products = {stripe.Product.retrieve(plan.product).id: plan.id for plan in plans}
return products
@property
def active_products(self):
products = cache.get(f"active_products_{self.id}")
if products is not None:
return products
products = self.get_active_products()
cache.set(f"active_products_{self.id}", products, 3600)
return products
@property
def best_active_subscription(self):
subscriptions = self.active_subscriptions
return subscriptions[0]
@property
def best_active_product(self):
products = self.active_products
products.sort(key=lambda p: p.metadata.get('level', 0))
return products[0]
@property
def best_active_subscription(self):
subscription_id = self.plan_to_subscription[self.prodct_to_plan[self.best_active_product.id]]
return stripe.Subscription.retrieve(subscription_id)
@property
def config(self):
product = self.best_active_product
return product.metadata
def subscription_status(self):
subscriptions = self.active_subscriptions()
def cancel_subscription(self):
for subscription in self.active_subscriptions:
stripe.Subscription.modify(
subscription.id,
cancel_at_period_end=True,
)
self.refresh()
def reactivate_subscription(self):
for subscription in self.active_subscriptions:
stripe.Subscription.modify(
subscription.id,
cancel_at_period_end=False,
)
self.refresh()
Looking for a Heroku replacement, What I found was shocking!
I’ve been using tailwind for a few months now and I can still say I’m loving it. I’ve been using it to create some rapid prototypes that may or may not ever become something, a document that is likely to go to print (a resume), and some quick dashboards.
I started using Tailwind a few month back #
A few months back in september of 2023 I made a case for tailwindcss. And have been using it on quite a few projects since.
- values are well thought out
- it’s really easy to use
- classes that make sense
- tree shakable
fokais.com #
I started working on fokais.com only a few weeks ago, It’s going to be a SAS to make blogging easier. I’ve started hosting some tools for this blog that I really like that I think I can turn into a service. It’s been fantastic to quickly pump out new pages with tailwind.
htmx">HTMX #
tailwind and htmx are a match made in heaven. They both really lean on Location of Behavior over Separation of concerns. They do really well at making small components that you can throw on and endpoint and stack into any page. With tailwind I just configure it to look at all my templates, and I can guarantee that the styles will be in app.css, and all I need to do is add classes to my component.
Heres a sample component for a user widget that will go on every page. It has everything it needs right in the template.
<div
hx-swap-oob="outerHTML"
id="user-header"
class="absolute top-0 right-0 mt-8 mr-4"
>
<!--markata-attribution-->
{% if current_user %}
<!--markata-attribution-->
<details
<!--markata-attribution-->
id="user-header-details"
<!--markata-attribution-->
open
class="group list-none px-4 py-2 self-center justify-self-center bg-neutral-600/10 shadow-lg shadow-zinc-950/20 ring-2 ring-zinc-950/5 rounded-xl flex justify-center align-center flex-col"
>
<summary style="list-style-type: none">{{ current_user.username }}</summary>
<!--markata-attribution-->
<div class="hidden group-hover:block my-4">
<!--markata-attribution-->
<a
class="mt-6 px-4 py-2 rounded bg-purple-950/5 ring-2 ring-cyan-500/30 text-cyan-500 font-bold"
<!--markata-attribution-->
href="{{ url_for('get_logout') }}"
>
<!--markata-attribution-->
Logout
</a>
<!--markata-attribution-->
</div>
<!--markata-attribution-->
</details>
{% else %}
<a
href="{{ url_for('post_login') }}"
class="mt-5 text-xl text-white font-bold text-shadow-xl text-shadow-zinc-950"
>
<!--markata-attribution-->
login
</a>
<!--markata-attribution-->
{% endif %}
<!--markata-attribution-->
</div>
internal apps #
I’ve built several interal apps, and tailwind has been really great for this. Its super quick to pop classes on components and get things to look decent quickly, or put some real polish into making them look nice.
My Website waylonwalker.com #
I’ve dropped my old decrepid css for some tailwind on my main site. My css was much smaller, but did not work quite as well on all devices, and most importantly was becoming a house of cards. Every time I fixed one thing several other things would fail. Colors were a bit muddy, and not as nicely configured as tailwind.
Most importantly was becoming a house of cards. Every time I fixed one thing several other things would fail.
One rough side of styling a blog in tailwind is that you don’t necessarily have control over granular details of how your pages get rendered without getting really deep into the markdown renderer, or writing your posts in html. It ends up looking a bit ugly, and is against the tailwind best practices, but it seems like the best way for a site like this.
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "./highlight.css";
.social {
@apply font-bold;
@apply flex flex-row;
@apply gap-4;
@apply justify-center;
@apply py-8;
}
#posts ul ul {
@apply backdrop-blur-sm;
@apply flex flex-col sm:grid grid-flow-row-dense;
@apply gap-4;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
@apply p-4;
}
grid #
I’ve struggled to use grid on my projects, and I’ve tried a few different times with no real success or adoption, but started using it on my resume, to have a main middle column, with two outer full bleed columns where I can make some elements full bleed to the edge. tailwind made this easy, once done, I had an admonition that was beautiful full bleed with a touch of color.