2022-12-20 00:11:34 +03:00
package routes
import (
2022-12-21 01:08:59 +03:00
"encoding/json"
"errors"
2022-12-20 00:11:34 +03:00
"fmt"
2022-12-21 01:08:59 +03:00
"github.com/emvi/logbuch"
2023-03-03 22:40:50 +03:00
"github.com/go-chi/chi/v5"
2022-12-20 00:11:34 +03:00
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/middlewares"
2022-12-23 00:43:41 +03:00
"github.com/muety/wakapi/models"
2023-01-02 20:05:28 +03:00
routeutils "github.com/muety/wakapi/routes/utils"
2022-12-20 00:11:34 +03:00
"github.com/muety/wakapi/services"
"github.com/stripe/stripe-go/v74"
2022-12-21 01:08:59 +03:00
stripePortalSession "github.com/stripe/stripe-go/v74/billingportal/session"
stripeCheckoutSession "github.com/stripe/stripe-go/v74/checkout/session"
stripeCustomer "github.com/stripe/stripe-go/v74/customer"
stripePrice "github.com/stripe/stripe-go/v74/price"
"github.com/stripe/stripe-go/v74/webhook"
"io/ioutil"
2022-12-20 00:11:34 +03:00
"net/http"
2022-12-23 12:54:56 +03:00
"strings"
2022-12-20 00:11:34 +03:00
"time"
)
2022-12-30 15:14:24 +03:00
/ *
How to integrate with Stripe ?
-- -
1. Create a plan with recurring payment ( https : //dashboard.stripe.com/test/products?active=true), copy its ID and save it as 'standard_price_id'
2. Create a webhook ( https : //dashboard.stripe.com/test/webhooks), with target URL '/subscription/webhook' and events ['customer.subscription.created', 'customer.subscription.updated', 'customer.subscription.deleted', 'checkout.session.completed'], copy the endpoint secret and save it to 'stripe_endpoint_secret'
3. Create a secret API key ( https : //dashboard.stripe.com/test/apikeys), copy it and save it to 'stripe_secret_key'
4. Copy the publishable API key ( https : //dashboard.stripe.com/test/apikeys) and save it to 'stripe_api_key'
* /
2022-12-20 00:11:34 +03:00
type SubscriptionHandler struct {
2022-12-29 19:12:34 +03:00
config * conf . Config
userSrvc services . IUserService
mailSrvc services . IMailService
keyValueSrvc services . IKeyValueService
httpClient * http . Client
2022-12-20 00:11:34 +03:00
}
func NewSubscriptionHandler (
userService services . IUserService ,
mailService services . IMailService ,
2022-12-29 19:12:34 +03:00
keyValueService services . IKeyValueService ,
2022-12-20 00:11:34 +03:00
) * SubscriptionHandler {
config := conf . Get ( )
if config . Subscriptions . Enabled {
2022-12-21 01:08:59 +03:00
stripe . Key = config . Subscriptions . StripeSecretKey
price , err := stripePrice . Get ( config . Subscriptions . StandardPriceId , nil )
if err != nil {
logbuch . Fatal ( "failed to fetch stripe plan details: %v" , err )
}
2022-12-23 12:54:56 +03:00
config . Subscriptions . StandardPrice = strings . TrimSpace ( fmt . Sprintf ( "%2.f €" , price . UnitAmountDecimal / 100.0 ) ) // TODO: respect actual currency
2022-12-21 01:08:59 +03:00
logbuch . Info ( "enabling subscriptions with stripe payment for %s / month" , config . Subscriptions . StandardPrice )
2022-12-20 00:11:34 +03:00
}
return & SubscriptionHandler {
2022-12-29 19:12:34 +03:00
config : config ,
userSrvc : userService ,
mailSrvc : mailService ,
keyValueSrvc : keyValueService ,
httpClient : & http . Client { Timeout : 10 * time . Second } ,
2022-12-20 00:11:34 +03:00
}
}
// https://stripe.com/docs/billing/quickstart?lang=go
2023-03-03 22:40:50 +03:00
func ( h * SubscriptionHandler ) RegisterRoutes ( router chi . Router ) {
2022-12-20 00:11:34 +03:00
if ! h . config . Subscriptions . Enabled {
return
}
2023-03-03 22:40:50 +03:00
subRouterPublic := chi . NewRouter ( )
subRouterPublic . Get ( "/success" , h . GetCheckoutSuccess )
subRouterPublic . Get ( "/cancel" , h . GetCheckoutCancel )
subRouterPublic . Post ( "/webhook" , h . PostWebhook )
2022-12-21 01:08:59 +03:00
2023-03-03 22:40:50 +03:00
subRouterPrivate := chi . NewRouter ( )
2022-12-21 01:08:59 +03:00
subRouterPrivate . Use (
2023-01-02 20:05:28 +03:00
middlewares . NewAuthenticateMiddleware ( h . userSrvc ) .
WithRedirectTarget ( defaultErrorRedirectTarget ( ) ) .
2023-03-03 22:40:50 +03:00
WithRedirectErrorMessage ( "unauthorized" ) . Handler ,
2022-12-20 00:11:34 +03:00
)
2023-03-03 22:40:50 +03:00
subRouterPrivate . Post ( "/checkout" , h . PostCheckout )
subRouterPrivate . Post ( "/portal" , h . PostPortal )
subRouterPublic . Mount ( "/" , subRouterPrivate )
router . Mount ( "/subscription" , subRouterPublic )
2022-12-20 00:11:34 +03:00
}
func ( h * SubscriptionHandler ) PostCheckout ( w http . ResponseWriter , r * http . Request ) {
if h . config . IsDev ( ) {
loadTemplates ( )
}
user := middlewares . GetPrincipal ( r )
if user . Email == "" {
2023-01-02 20:05:28 +03:00
routeutils . SetError ( r , w , "missing e-mail address" )
http . Redirect ( w , r , fmt . Sprintf ( "%s/settings#subscription" , h . config . Server . BasePath ) , http . StatusFound )
2022-12-20 00:11:34 +03:00
return
}
if err := r . ParseForm ( ) ; err != nil {
2023-01-02 20:05:28 +03:00
routeutils . SetError ( r , w , "missing form values" )
http . Redirect ( w , r , fmt . Sprintf ( "%s/settings#subscription" , h . config . Server . BasePath ) , http . StatusFound )
2022-12-20 00:11:34 +03:00
return
}
checkoutParams := & stripe . CheckoutSessionParams {
Mode : stripe . String ( string ( stripe . CheckoutSessionModeSubscription ) ) ,
LineItems : [ ] * stripe . CheckoutSessionLineItemParams {
{
Price : & h . config . Subscriptions . StandardPriceId ,
Quantity : stripe . Int64 ( 1 ) ,
} ,
} ,
2022-12-30 15:14:24 +03:00
ClientReferenceID : & user . ID ,
2022-12-21 01:08:59 +03:00
SuccessURL : stripe . String ( fmt . Sprintf ( "%s%s/subscription/success" , h . config . Server . PublicUrl , h . config . Server . BasePath ) ) ,
CancelURL : stripe . String ( fmt . Sprintf ( "%s%s/subscription/cancel" , h . config . Server . PublicUrl , h . config . Server . BasePath ) ) ,
2022-12-20 00:11:34 +03:00
}
2022-12-30 15:14:24 +03:00
if user . StripeCustomerId != "" {
checkoutParams . Customer = & user . StripeCustomerId
} else {
checkoutParams . CustomerEmail = & user . Email
}
2022-12-21 01:08:59 +03:00
session , err := stripeCheckoutSession . New ( checkoutParams )
2022-12-20 00:11:34 +03:00
if err != nil {
conf . Log ( ) . Request ( r ) . Error ( "failed to create stripe checkout session: %v" , err )
2023-01-02 20:05:28 +03:00
routeutils . SetError ( r , w , "something went wrong" )
http . Redirect ( w , r , fmt . Sprintf ( "%s/settings#subscription" , h . config . Server . BasePath ) , http . StatusFound )
2022-12-20 00:11:34 +03:00
return
}
http . Redirect ( w , r , session . URL , http . StatusSeeOther )
}
func ( h * SubscriptionHandler ) PostPortal ( w http . ResponseWriter , r * http . Request ) {
if h . config . IsDev ( ) {
loadTemplates ( )
}
user := middlewares . GetPrincipal ( r )
2022-12-30 15:14:24 +03:00
if user . StripeCustomerId == "" {
2023-01-02 20:05:28 +03:00
routeutils . SetError ( r , w , "no subscription found with your e-mail address, please contact us!" )
http . Redirect ( w , r , fmt . Sprintf ( "%s/settings#subscription" , h . config . Server . BasePath ) , http . StatusFound )
2022-12-20 00:11:34 +03:00
return
}
portalParams := & stripe . BillingPortalSessionParams {
2022-12-30 15:14:24 +03:00
Customer : & user . StripeCustomerId ,
2022-12-20 00:11:34 +03:00
ReturnURL : & h . config . Server . PublicUrl ,
}
2022-12-21 01:08:59 +03:00
session , err := stripePortalSession . New ( portalParams )
2022-12-20 00:11:34 +03:00
if err != nil {
conf . Log ( ) . Request ( r ) . Error ( "failed to create stripe portal session: %v" , err )
2023-01-02 20:05:28 +03:00
routeutils . SetError ( r , w , "something went wrong" )
http . Redirect ( w , r , fmt . Sprintf ( "%s/settings#subscription" , h . config . Server . BasePath ) , http . StatusFound )
2022-12-20 00:11:34 +03:00
return
}
http . Redirect ( w , r , session . URL , http . StatusSeeOther )
}
func ( h * SubscriptionHandler ) PostWebhook ( w http . ResponseWriter , r * http . Request ) {
2022-12-21 01:08:59 +03:00
bodyReader := http . MaxBytesReader ( w , r . Body , int64 ( 65536 ) )
payload , err := ioutil . ReadAll ( bodyReader )
if err != nil {
conf . Log ( ) . Request ( r ) . Error ( "error in stripe webhook request: %v" , err )
w . WriteHeader ( http . StatusServiceUnavailable )
return
}
event , err := webhook . ConstructEventWithOptions ( payload , r . Header . Get ( "Stripe-Signature" ) , h . config . Subscriptions . StripeEndpointSecret , webhook . ConstructEventOptions {
IgnoreAPIVersionMismatch : true ,
} )
if err != nil {
conf . Log ( ) . Request ( r ) . Error ( "stripe webhook signature verification failed: %v" , err )
w . WriteHeader ( http . StatusBadRequest )
return
}
switch event . Type {
case "customer.subscription.deleted" ,
"customer.subscription.updated" ,
"customer.subscription.created" :
2022-12-23 00:43:41 +03:00
// example payload: https://pastr.de/p/k7bx3alx38b1iawo6amtx09k
2022-12-30 15:14:24 +03:00
subscription , err := h . parseSubscriptionEvent ( w , r , event )
2022-12-21 01:08:59 +03:00
if err != nil {
2022-12-30 15:14:24 +03:00
w . WriteHeader ( http . StatusInternalServerError )
2022-12-21 01:08:59 +03:00
return
}
2022-12-30 15:14:24 +03:00
logbuch . Info ( "received stripe subscription event of type '%s' for subscription '%s' (customer '%s')." , event . Type , subscription . ID , subscription . Customer . ID )
2022-12-23 00:43:41 +03:00
2022-12-30 15:14:24 +03:00
// first, try to get user by associated customer id (requires checkout.session.completed event to have been processed before)
user , err := h . userSrvc . GetUserByStripeCustomerId ( subscription . Customer . ID )
2022-12-23 00:43:41 +03:00
if err != nil {
2022-12-30 15:14:24 +03:00
conf . Log ( ) . Request ( r ) . Warn ( "failed to find user with stripe customer id '%s' to update their subscription (status '%s')" , subscription . Customer . ID , subscription . Status )
// second, resolve customer and try to get user by email
customer , err := stripeCustomer . Get ( subscription . Customer . ID , nil )
if err != nil {
conf . Log ( ) . Request ( r ) . Error ( "failed to fetch stripe customer with id '%s', %v" , subscription . Customer . ID , err )
w . WriteHeader ( http . StatusInternalServerError )
return
}
u , err := h . userSrvc . GetUserByEmail ( customer . Email )
if err != nil {
conf . Log ( ) . Request ( r ) . Error ( "failed to get user with email '%s' as stripe customer '%s' for processing event for subscription %s, %v" , customer . Email , subscription . Customer . ID , subscription . ID , err )
w . WriteHeader ( http . StatusInternalServerError )
return
}
user = u
2022-12-23 00:43:41 +03:00
}
if err := h . handleSubscriptionEvent ( subscription , user ) ; err != nil {
conf . Log ( ) . Request ( r ) . Error ( "failed to handle subscription event %s (%s) for user %s, %v" , event . ID , event . Type , user . ID , err )
w . WriteHeader ( http . StatusInternalServerError )
return
}
2022-12-30 15:14:24 +03:00
case "checkout.session.completed" :
// example payload: https://pastr.de/p/d01iniw9naq9hkmvyqtxin2w
checkoutSession , err := h . parseCheckoutSessionEvent ( w , r , event )
if err != nil {
w . WriteHeader ( http . StatusInternalServerError )
return
}
logbuch . Info ( "received stripe checkout session event of type '%s' for session '%s' (customer '%s' with email '%s')." , event . Type , checkoutSession . ID , checkoutSession . Customer . ID , checkoutSession . CustomerEmail )
user , err := h . userSrvc . GetUserById ( checkoutSession . ClientReferenceID )
if err != nil {
conf . Log ( ) . Request ( r ) . Error ( "failed to find user with id '%s' to update associated stripe customer (%s)" , user . ID , checkoutSession . Customer . ID )
w . WriteHeader ( http . StatusInternalServerError )
return
}
if user . StripeCustomerId == "" {
user . StripeCustomerId = checkoutSession . Customer . ID
if _ , err := h . userSrvc . Update ( user ) ; err != nil {
conf . Log ( ) . Request ( r ) . Error ( "failed to update stripe customer id (%s) for user '%s', %v" , checkoutSession . Customer . ID , user . ID , err )
} else {
logbuch . Info ( "associated user '%s' with stripe customer '%s'" , user . ID , checkoutSession . Customer . ID )
}
} else if user . StripeCustomerId != checkoutSession . Customer . ID {
conf . Log ( ) . Request ( r ) . Error ( "invalid state: tried to associate user '%s' with stripe customer '%s', but '%s' already assigned" , user . ID , checkoutSession . Customer . ID , user . StripeCustomerId )
}
2022-12-21 01:08:59 +03:00
default :
logbuch . Warn ( "got stripe event '%s' with no handler defined" , event . Type )
}
w . WriteHeader ( http . StatusOK )
2022-12-20 00:11:34 +03:00
}
func ( h * SubscriptionHandler ) GetCheckoutSuccess ( w http . ResponseWriter , r * http . Request ) {
2023-01-02 20:05:28 +03:00
routeutils . SetSuccess ( r , w , "you have successfully subscribed to Wakapi!" )
http . Redirect ( w , r , fmt . Sprintf ( "%s/settings" , h . config . Server . BasePath ) , http . StatusFound )
2022-12-20 00:11:34 +03:00
}
func ( h * SubscriptionHandler ) GetCheckoutCancel ( w http . ResponseWriter , r * http . Request ) {
http . Redirect ( w , r , fmt . Sprintf ( "%s/settings#subscription" , h . config . Server . BasePath ) , http . StatusFound )
}
2022-12-21 01:08:59 +03:00
2022-12-23 00:43:41 +03:00
func ( h * SubscriptionHandler ) handleSubscriptionEvent ( subscription * stripe . Subscription , user * models . User ) error {
2022-12-29 19:12:34 +03:00
var hasSubscribed bool
2022-12-23 00:43:41 +03:00
switch subscription . Status {
case "active" :
until := models . CustomTime ( time . Unix ( subscription . CurrentPeriodEnd , 0 ) )
if user . SubscribedUntil == nil || ! user . SubscribedUntil . T ( ) . Equal ( until . T ( ) ) {
2022-12-29 19:12:34 +03:00
hasSubscribed = true
2022-12-23 00:43:41 +03:00
user . SubscribedUntil = & until
2023-02-19 21:37:03 +03:00
user . SubscriptionRenewal = & until
2022-12-23 00:43:41 +03:00
logbuch . Info ( "user %s got active subscription %s until %v" , user . ID , subscription . ID , user . SubscribedUntil )
}
2022-12-30 15:14:24 +03:00
if cancelAt := time . Unix ( subscription . CancelAt , 0 ) ; ! cancelAt . IsZero ( ) && cancelAt . After ( time . Now ( ) ) {
2023-02-19 21:37:03 +03:00
user . SubscriptionRenewal = nil
2022-12-23 00:43:41 +03:00
logbuch . Info ( "user %s chose to cancel subscription %s by %v" , user . ID , subscription . ID , cancelAt )
}
2023-02-19 21:37:03 +03:00
case "canceled" , "unpaid" , "incomplete_expired" :
2022-12-23 00:43:41 +03:00
user . SubscribedUntil = nil
2023-02-19 21:37:03 +03:00
user . SubscriptionRenewal = nil
2022-12-23 00:43:41 +03:00
logbuch . Info ( "user %s's subscription %s got canceled, because of status update to '%s'" , user . ID , subscription . ID , subscription . Status )
default :
logbuch . Info ( "got subscription (%s) status update to '%s' for user '%s'" , subscription . ID , subscription . Status , user . ID )
2022-12-30 15:14:24 +03:00
return nil
2022-12-23 00:43:41 +03:00
}
_ , err := h . userSrvc . Update ( user )
2022-12-29 19:12:34 +03:00
if err == nil && hasSubscribed {
go h . clearSubscriptionNotificationStatus ( user . ID )
}
2022-12-23 00:43:41 +03:00
return err
}
2022-12-30 15:14:24 +03:00
func ( h * SubscriptionHandler ) parseSubscriptionEvent ( w http . ResponseWriter , r * http . Request , event stripe . Event ) ( * stripe . Subscription , error ) {
2022-12-21 01:08:59 +03:00
var subscription stripe . Subscription
if err := json . Unmarshal ( event . Data . Raw , & subscription ) ; err != nil {
conf . Log ( ) . Request ( r ) . Error ( "failed to parse stripe webhook payload: %v" , err )
w . WriteHeader ( http . StatusBadRequest )
2022-12-30 15:14:24 +03:00
return nil , err
2022-12-21 01:08:59 +03:00
}
2022-12-30 15:14:24 +03:00
return & subscription , nil
}
2022-12-21 01:08:59 +03:00
2022-12-30 15:14:24 +03:00
func ( h * SubscriptionHandler ) parseCheckoutSessionEvent ( w http . ResponseWriter , r * http . Request , event stripe . Event ) ( * stripe . CheckoutSession , error ) {
var checkoutSession stripe . CheckoutSession
if err := json . Unmarshal ( event . Data . Raw , & checkoutSession ) ; err != nil {
conf . Log ( ) . Request ( r ) . Error ( "failed to parse stripe webhook payload: %v" , err )
2022-12-21 01:08:59 +03:00
w . WriteHeader ( http . StatusBadRequest )
2022-12-30 15:14:24 +03:00
return nil , err
2022-12-21 01:08:59 +03:00
}
2022-12-30 15:14:24 +03:00
return & checkoutSession , nil
2022-12-21 01:08:59 +03:00
}
func ( h * SubscriptionHandler ) findStripeCustomerByEmail ( email string ) ( * stripe . Customer , error ) {
params := & stripe . CustomerSearchParams {
SearchParams : stripe . SearchParams {
Query : fmt . Sprintf ( ` email:"%s" ` , email ) ,
} ,
}
results := stripeCustomer . Search ( params )
if err := results . Err ( ) ; err != nil {
return nil , err
}
if results . Next ( ) {
return results . Customer ( ) , nil
} else {
return nil , errors . New ( "no customer found with given criteria" )
}
}
2022-12-29 19:12:34 +03:00
func ( h * SubscriptionHandler ) clearSubscriptionNotificationStatus ( userId string ) {
key := fmt . Sprintf ( "%s_%s" , conf . KeySubscriptionNotificationSent , userId )
if err := h . keyValueSrvc . DeleteString ( key ) ; err != nil {
2022-12-30 15:14:24 +03:00
logbuch . Warn ( "failed to delete '%s', %v" , key , err )
2022-12-29 19:12:34 +03:00
}
}