Skip to content

Commit

Permalink
Added ApplePay PaymentSession Request
Browse files Browse the repository at this point in the history
  • Loading branch information
rgaudin committed Nov 20, 2024
1 parent 9e11446 commit ea3573b
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 4 deletions.
25 changes: 22 additions & 3 deletions donation-api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ COPY pyproject.toml README.md /src/
COPY src/donation_api/__about__.py /src/src/donation_api/__about__.py

# Install Python dependencies
RUN pip install --no-cache-dir /src
RUN apk --no-cache add dumb-init \
&& pip install --no-cache-dir /src

COPY src /src/src
COPY *.md /src/
COPY entrypoint.sh /usr/local/bin/entrypoint

# Install + cleanup
RUN pip install --no-cache-dir /src \
Expand All @@ -21,8 +23,25 @@ RUN pip install --no-cache-dir /src \

# set STRIPE_USE_LIVE=1 for production (use of live key)
ENV STRIPE_USE_LIVE=0
ENV STRIPE_TEST_KEY=notset
ENV STRIPE_LIVE_KEY=notset
ENV STRIPE_TEST_PUBLISHABLE_KEY=notset
ENV STRIPE_TEST_SECRET_KEY=notset
ENV STRIPE_LIVE_PUBLISHABLE_KEY=notset
ENV STRIPE_LIVE_SECRET_KEY=notset
ENV STRIPE_WEBHOOK_SECRET=""
ENV STRIPE_MINIMAL_AMOUNT=5
ENV STRIPE_MAXIMUM_AMOUNT=999999
ENV STRIPE_WEBHOOK_TESTING_IPS=
ENV ALLOWED_CURRENCIES=chf|usd|eur

ENV MERCHANTID_DOMAIN_ASSOCIATION=
ENV MERCHANTID_DOMAIN_ASSOCIATION_TXT=
ENV APPLEPAY_MERCHANT_IDENTIFIER=
ENV APPLEPAY_DISPLAYNAME=
ENV APPLEPAY_PAYMENT_SESSION_INITIATIVE=
ENV APPLEPAY_PAYMENT_SESSION_INITIATIVE_CONTEXT=
# ENV APPLEPAY_PAYMENT_SESSION_REQ_TIMEOUT_SEC=5
ENV APPLEPAY_MERCHANT_CERTIFICATE_PATH=/etc/ssl/certs/applepay_merchant.pem
ENV APPLEPAY_MERCHANT_CERTIFICATE_KEY_PATH=/etc/ssl/certs/applepay_merchant.key

ENTRYPOINT ["/usr/bin/dumb-init", "--", "/usr/local/bin/entrypoint"]
CMD ["uvicorn", "donation_api.entrypoint:app", "--host", "0.0.0.0", "--port", "80"]
3 changes: 2 additions & 1 deletion donation-api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ description = "A simple Stripe relay endpoint"
readme = "README.md"
dependencies = [
"stripe==11.2.0",
"fastapi[standard]==0.115.5"
"fastapi[standard]==0.115.5",
"requests==2.32.3",
]
dynamic = ["authors", "classifiers", "keywords", "license", "version", "urls"]

Expand Down
34 changes: 34 additions & 0 deletions donation-api/src/donation_api/constants.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import pathlib
from dataclasses import dataclass, field

import requests
Expand Down Expand Up @@ -26,6 +27,20 @@ class Constants:
os.getenv("MERCHANTID_DOMAIN_ASSOCIATION_TXT") or ""
)

applepay_merchant_identifier: str = os.getenv("APPLEPAY_MERCHANT_IDENTIFIER") or ""
applepay_displayname: str = os.getenv("APPLEPAY_DISPLAYNAME") or ""
applepay_payment_session_initiative: str = (
os.getenv("APPLEPAY_PAYMENT_SESSION_INITIATIVE") or ""
)
applepay_payment_session_initiative_context: str = (
os.getenv("APPLEPAY_PAYMENT_SESSION_INITIATIVE_CONTEXT") or ""
)
applepay_merchant_certificate_path: pathlib.Path = pathlib.Path("/missing")
applepay_merchant_certificate_key_path: pathlib.Path = pathlib.Path("/missing")
applepay_payment_session_request_timeout: int = int(
os.getenv("APPLEPAY_PAYMENT_SESSION_REQ_TIMEOUT_SEC") or "5"
)

stripe_minimal_amount: int = int(os.getenv("STRIPE_MINIMAL_AMOUNT") or "5")
stripe_maximum_amount: int = int(os.getenv("STRIPE_MAXIMUM_AMOUNT") or "999999")

Expand All @@ -44,6 +59,25 @@ def __post_init__(self):
if not self.stripe_webhook_sender_ips:
raise OSError("No Stripe Webhook IPs!")

if (
self.applepay_payment_session_initiative
and self.applepay_payment_session_initiative not in ("web", "in_app")
):
raise OSError("ApplePay Payment Session Initiative in invalid")

if (
self.applepay_payment_session_initiative
and not self.applepay_payment_session_initiative_context
):
raise OSError("Missing ApplePay Payment Initiative Context")

certpath = os.getenv("APPLEPAY_MERCHANT_CERTIFICATE_PATH") or ""
if certpath:
self.applepay_merchant_certificate_path = pathlib.Path(certpath)
certkeypath = os.getenv("APPLEPAY_MERCHANT_CERTIFICATE_KEY_PATH") or ""
if certkeypath:
self.applepay_merchant_certificate_key_path = pathlib.Path(certpath)

@property
def stripe_secret_api_key(self) -> str:
if self.stripe_on_prod:
Expand Down
67 changes: 67 additions & 0 deletions donation-api/src/donation_api/stripe.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from http import HTTPStatus
from typing import Annotated, Any

import requests
import stripe
from fastapi import APIRouter, Depends, Header, HTTPException, Request
from pydantic import BaseModel, ConfigDict
Expand Down Expand Up @@ -60,6 +61,10 @@ class StripeWebhookResponse(BaseModel):
status: str


class OpaqueApplePayPaymentSession(BaseModel):
model_config = ConfigDict(extra="allow")


async def get_body(request: Request):
"""raw request body"""
return await request.body()
Expand Down Expand Up @@ -128,6 +133,24 @@ async def check_config():
if not conf.alllowed_currencies:
errors.append("Missing currencies list")

if not conf.applepay_merchant_identifier:
errors.append("Missing ApplePay merchantIdentifier")

if not conf.applepay_displayname:
errors.append("Missing ApplePay displayName")

if not conf.applepay_payment_session_initiative:
errors.append("Missing ApplePay session initiative")

if not conf.applepay_payment_session_initiative_context:
errors.append("Missing ApplePay session initiative context")

if not conf.applepay_merchant_certificate_path.read_text():
errors.append("Missing ApplePay merchant certificate")

if not conf.applepay_merchant_certificate_key_path.read_text():
errors.append("Missing ApplePay merchant certificate key")

if errors:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="\n".join(errors)
Expand Down Expand Up @@ -247,3 +270,47 @@ def webhook_received(
logger.info("❌ Payment failed.")

return {"status": "success"}


@router.post(
"/payment-session",
responses={
HTTPStatus.BAD_REQUEST: {
"description": "Request for a Payment Session from ApplePay failed",
},
HTTPStatus.OK: {
"model": OpaqueApplePayPaymentSession,
"description": "ApplePay Server returned an Opaque Payment Session",
},
},
status_code=HTTPStatus.OK,
)
async def create_payment_session():
payload = {
"merchantIdentifier": conf.applepay_merchant_identifier,
"displayName": conf.applepay_displayname,
"initiative": conf.applepay_payment_session_initiative,
"initiativeContext": conf.applepay_payment_session_initiative_context,
}

data: dict[str, Any] = {}
resp = requests.post(
url="https://apple-pay-gateway.apple.com/paymentservices/paymentSession",
cert=(
str(conf.applepay_merchant_certificate_path),
str(conf.applepay_merchant_certificate_key_path),
),
json=payload,
timeout=conf.applepay_payment_session_request_timeout,
)
try:
data = resp.json()
except Exception:
...
if resp.status_code != HTTPStatus.OK:
raise HTTPException(
status_code=resp.status_code,
detail=data.get("statusMessage") or "Failed to request payment session",
)

return data

0 comments on commit ea3573b

Please sign in to comment.