Skip to content

Commit a1f3fae

Browse files
committed
feat: setup stripe subscriptions
The abstractions are leaky though. Unless you add the concepts of a customer/id and price_id to the basic subscription domain. app/subscription is for most subscription apis, but there's also app/subscription_portal which just sends users to stripe to handle checkout. Webhooks are the same either way, but if developing the UI in-house, you wouldn't use app/subscription_portal services.
1 parent 58dd279 commit a1f3fae

35 files changed

+788
-93
lines changed

.devcontainer/.env.example

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
1+
# This file is used by ./docker-compose.yml.
2+
13
# Host ports
2-
HOST_API_PORT=8000
4+
#
5+
# For avoiding conflicts on the host machine.
6+
HOST_API_DEV_PORT=8000 # Dev server
7+
HOST_API_DOCKER_PORT=8001 # Docker preview
8+
HOST_API_SAM_PORT=8002 # AWS SAM preview
39
HOST_POSTGRES_PORT=5432
410
HOST_NEO4J_PORT=7687
5-
HOST_NEO4J_BROWSER_PORT=7474
11+
HOST_NEO4J_BROWSER_PORT=7474
12+
13+
# Stripe test keys
14+
#
15+
# These can be shared with developers, but should not be public.
16+
STRIPE_PUBLIC_KEY=pk_test_ # From Stripe dashboard
17+
STRIPE_SECRET_KEY=sk_test_ # From Stripe dashboard
18+
STRIPE_WEBHOOK_SECRET=whsec_ # From Stripe CLI

.devcontainer/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ FROM jifalops/python-lambda:latest
55
RUN export DEBIAN_FRONTEND=noninteractive && sudo apt-get update && sudo apt-get install -y --no-install-recommends \
66
trash-cli
77

8-
# Install neo4j-migrations
8+
# Install neo4j-migrations (The SDKMAN installer on https://containers.dev/features is extremely slow)
99
RUN VERSION=$(basename $(curl -Ls -o /dev/null -w %{url_effective} https://github.com/michael-simons/neo4j-migrations/releases/latest)) && \
1010
ZIP_LINUX=https://github.com/michael-simons/neo4j-migrations/releases/download/${VERSION}/neo4j-migrations-${VERSION}-linux-x86_64.zip && \
1111
curl -L $ZIP_LINUX -o nmig.zip && \

.devcontainer/devcontainer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
"initializeCommand": ".devcontainer/host-init.sh",
88
"postCreateCommand": ".devcontainer/post-create.sh",
99
"postStartCommand": ".devcontainer/post-start.sh",
10+
"features": {
11+
"ghcr.io/nullcoder/devcontainer-features/stripe-cli:1": {}
12+
},
1013
"customizations": {
1114
"vscode": {
1215
"settings": {

.devcontainer/docker-compose.yml

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,30 @@ services:
44
context: ..
55
dockerfile: .devcontainer/Dockerfile
66
ports:
7-
- ${HOST_API_PORT}:8000
7+
- ${HOST_API_DEV_PORT}:8000
8+
- ${HOST_API_DOCKER_PORT}:8001
9+
- ${HOST_API_SAM_PORT}:8002
810
command: sleep infinity
911
depends_on:
1012
- postgres
1113
- neo4j
1214
volumes:
1315
- ..:/api-python:cached
1416
environment:
15-
TZ: ${TZ} # Timezone
17+
# From host machine
18+
TZ: ${TZ} # Timezone
1619
GITHUB_TOKEN: ${GITHUB_TOKEN} # Github CLI
17-
LOGGING_LEVEL: DEBUG # Python logging
18-
HOST_API_PORT: ${HOST_API_PORT}
20+
# Python
21+
LOGGING_LEVEL: DEBUG
22+
# Postgres
1923
POSTGRES_URI: postgresql://postgres:developer@postgres:5432/postgres
24+
# Neo4j
2025
NEO4J_URI: neo4j://neo4j:7687
2126
NEO4J_PASSWORD: developer
27+
# Stripe (test mode)
28+
STRIPE_PUBLIC_KEY: ${STRIPE_PUBLIC_KEY}
29+
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
30+
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET}
2231

2332
postgres:
2433
image: postgres

.devcontainer/post-create.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,8 @@ fi
2121

2222
# Activate the virtual environment.
2323
echo "source \"$(pwd)/.venv/bin/activate\"" >> ~/.bashrc
24+
25+
# Add bash completion for the Stripe CLI.
26+
mkdir -p ~/.local/share/bash-completion/completions && \
27+
stripe completion --shell bash && \
28+
mv stripe-completion.bash ~/.local/share/bash-completion/completions/stripe

.env.example

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,34 @@
1-
# Define development environment variables in a bash-compliant format.
1+
# This file is sourced by bash scripts in the ./scripts/admin directory.
22

3-
# AWS config for local administration.
3+
# Config for the AWS and SAM CLIs (`aws` and `sam`).
44
AWS_ACCESS_KEY_ID=''
55
AWS_SECRET_ACCESS_KEY=''
66
AWS_DEFAULT_REGION=''
77
AWS_DEFAULT_OUTPUT='json'
88

9-
# Visit the endpoint at least once to generate some logs, then:
10-
# aws logs describe-log-groups | grep api-python
11-
PROD_LAMBDA_LOG_GROUP='api-python-ApiFunction-XXXXXXXXXXXX'
9+
# Environment variables for the API when deployed manually.
10+
PROD_LOGGING_LEVEL='INFO'
11+
PROD_WORKERS=1
12+
PROD_POSTGRES_URI=''
13+
PROD_NEO4J_URI=''
14+
PROD_NEO4J_PASSWORD=''
1215

16+
# The API Gateway endpoint URL. Clients can use this to interact with the API.
17+
#
18+
# ```sh
1319
# aws apigatewayv2 get-apis --query 'Items[?Name==`api-python`].ApiEndpoint' --output text
14-
PROD_API_URL=''
20+
# ```
21+
PROD_API_URL_AWS=''
1522

16-
PROD_LOGGING_LEVEL='INFO'
17-
PROD_WORKERS=1
18-
PROD_POSTGRES_URI='postgres://postgres:password@host:5432/postgres'
19-
PROD_NEO4J_URI='neo4j://neo4j:7687'
20-
PROD_NEO4J_PASSWORD='password'
23+
# The CloudWatch log group for the Lambda function.
24+
#
25+
# You may need to visit the API endpoint before logs are generated.
26+
#
27+
# ```sh
28+
# aws logs describe-log-groups | grep api-python
29+
# ```
30+
PROD_LAMBDA_LOG_GROUP=''
31+
32+
# Stripe production keys
33+
PROD_STRIPE_SECRET_KEY=''
34+
PROD_STRIPE_WEBHOOK_SECRET=''

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ __pycache__
77
.env
88
.env.*
99
!.env.example
10-
.aws-sam
10+
.aws-sam
11+
Pipfile*

.vscode/settings.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,29 @@
3535
{
3636
"splitTerminals": [
3737
{
38-
"name": "preview Docker",
38+
"name": "Docker preview",
3939
"commands": [
4040
"./scripts/preview_docker.sh"
4141
]
4242
},
4343
{
44-
"name": "preview SAM",
44+
"name": "SAM preview",
4545
"commands": [
4646
"./scripts/preview_sam.sh"
4747
]
4848
},
4949
]
5050
},
51+
{
52+
"splitTerminals": [
53+
{
54+
"name": "stripe",
55+
"commands": [
56+
"./scripts/stripe-listen.sh"
57+
]
58+
},
59+
]
60+
},
5161
{
5262
"splitTerminals": [
5363
{

app/app.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from dataclasses import dataclass
2-
from typing import List
32

43
from app.auth.service import AuthService
5-
from app.base_service import BaseService
4+
from app.service import Service
5+
from app.subscription.service import SubscriptionService
6+
from app.subscription_portal.service import SubscriptionPortalService
7+
from app.user.service import UserService
68

79

810
@dataclass
@@ -14,10 +16,16 @@ class App:
1416
"""
1517

1618
auth: AuthService
19+
subscription: SubscriptionService
20+
subscription_portal: SubscriptionPortalService
21+
user: UserService
1722

1823
def __post_init__(self):
19-
self._services: List[BaseService] = [
24+
self._services: list[Service] = [
2025
self.auth,
26+
self.subscription,
27+
self.subscription_portal,
28+
self.user,
2129
]
2230
for service in self._services:
2331
service._set_app(self) # type: ignore

app/auth/service.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
1+
import logging
2+
from typing import Optional
3+
14
import shortuuid
25

36
from app.auth.models import SignUpData
4-
from app.base_service import BaseService
7+
from app.service import Service
8+
from app.subscription.models import SubscriptionLevel
59

610

7-
class AuthService(BaseService):
11+
class AuthService(Service):
812
"""The service that contains the core business logic for authentication."""
913

1014
async def sign_up(self, data: SignUpData) -> str:
1115
return f"user_{shortuuid.uuid()}"
16+
17+
async def set_subscription_level(
18+
self, user_id: str, level: Optional[SubscriptionLevel]
19+
) -> None:
20+
logging.debug(f"Setting subscription level for user {user_id} to {level}")
21+
# raise NotImplementedError()

0 commit comments

Comments
 (0)