-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #45 from commonknowledge/feature/ck-3811-core-supp…
…ort-for-stripe-in-minimalist-join-form CK-3811 Core Support for Stripe in Minimalist Join Flow
- Loading branch information
Showing
5 changed files
with
414 additions
and
198 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,7 @@ | |
use CommonKnowledge\JoinBlock\Blocks; | ||
use CommonKnowledge\JoinBlock\Exceptions\SubscriptionExistsException; | ||
use CommonKnowledge\JoinBlock\Services\GocardlessService; | ||
use CommonKnowledge\JoinBlock\Services\StripeService; | ||
use CommonKnowledge\JoinBlock\Services\MailchimpService; | ||
use CommonKnowledge\JoinBlock\Settings; | ||
use Monolog\Logger; | ||
|
@@ -272,79 +273,38 @@ | |
'callback' => function (WP_REST_Request $request) { | ||
global $joinBlockLog; | ||
|
||
$joinBlockLog->info('Processing Stripe subscription creation request'); | ||
|
||
// This should be handled in the Stripe class, not here, but for now let's do it here | ||
Stripe::setApiKey(Settings::get('STRIPE_SECRET_KEY')); | ||
$data = json_decode($request->get_body(), true); | ||
|
||
$name = 'Giuseppe Pinot-Gallizio'; | ||
$email = '[email protected]'; | ||
$selectedPlanLabel = $data['membership']; | ||
|
||
$customers = Customer::all([ | ||
'email' => $email, | ||
'limit' => 1 // We just need the first match | ||
]); | ||
$joinBlockLog->info('Attempting to find a matching plan', ['selectedPlanLabel' => $selectedPlanLabel]); | ||
|
||
$newCustomer = false; | ||
$plan = Settings::getMembershipPlan($selectedPlanLabel); | ||
|
||
if (count($customers->data) > 0) { | ||
$customer = $customers->data[0]; | ||
if (!$plan) { | ||
throw new \Exception('Selected plan is not in the list of plans, this is unexpected'); | ||
} else { | ||
$newCustomer = true; | ||
$joinBlockLog->info('Found a matching plan in the list of plans', $plan); | ||
} | ||
|
||
$customer = Customer::create([ | ||
'email' => $email, | ||
'name' => $name | ||
]); | ||
$joinBlockLog->info('Processing Stripe subscription creation request'); | ||
|
||
$joinBlockLog->info('Customer created successfully! Customer ID: ' . $customer->id); | ||
} | ||
$email = $data['email']; | ||
|
||
$data = json_decode($request->get_body(), true); | ||
StripeService::initialise(); | ||
[$customer, $newCustomer] = StripeService::upsertCustomer($email); | ||
|
||
$joinBlockLog->info('Here is your Stripe data dump', $data); | ||
$subscription = StripeService::createSubscription($customer, $plan); | ||
|
||
$confirmedPaymentIntent = StripeService::confirmSubscriptionPaymentIntent($subscription, $data['confirmationTokenId']); | ||
|
||
StripeService::updateCustomerDefaultPaymentMethod($customer->id, $subscription->latest_invoice->payment_intent->payment_method); | ||
|
||
/* Try catch around this */ | ||
$subscription = Subscription::create([ | ||
'customer' => $customer->id, | ||
'items' => [ | ||
[ | ||
'price' => 'price_1PyB84ISmeoaI3mwaI1At8af', | ||
], | ||
], | ||
'payment_behavior'=> 'default_incomplete', | ||
'payment_settings' => ['save_default_payment_method' => 'on_subscription'], | ||
'expand' => ['latest_invoice.payment_intent'], | ||
]); | ||
|
||
// Need to handle this payment intent stuff??? | ||
$paymentIntentId = $subscription->latest_invoice->payment_intent->id; | ||
$paymentIntent = \Stripe\PaymentIntent::retrieve($paymentIntentId); | ||
|
||
/* Try catch around this */ | ||
$confirmedPaymentIntent = $paymentIntent->confirm([ | ||
'confirmation_token' => $data['confirmationTokenId'], | ||
]); | ||
|
||
$status = $confirmedPaymentIntent->status; | ||
|
||
$paymentMethodId = $subscription->latest_invoice->payment_intent->payment_method; | ||
|
||
/* Try catch around this */ | ||
Customer::update( | ||
$customer->id, | ||
[ | ||
'invoice_settings' => [ | ||
'default_payment_method' => $paymentMethodId, | ||
], | ||
] | ||
); | ||
|
||
return [ | ||
"status" => $status, | ||
"status" => $confirmedPaymentIntent->status, | ||
"new_customer" => $newCustomer, | ||
"customer" => $customer->toArray(), | ||
"subscription" => $subscription->toArray() | ||
"stripe_customer" => $customer->toArray(), | ||
"stripe_subscription" => $subscription->toArray() | ||
]; | ||
} | ||
)); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,226 @@ | ||
<?php | ||
|
||
namespace CommonKnowledge\JoinBlock\Services; | ||
|
||
use CommonKnowledge\JoinBlock\Settings; | ||
|
||
use Stripe\Stripe; | ||
use Stripe\Customer; | ||
use Stripe\Subscription; | ||
use Stripe\Exception\ApiErrorException; | ||
|
||
class StripeService | ||
{ | ||
public static function initialise() | ||
{ | ||
Stripe::setApiKey(Settings::get('STRIPE_SECRET_KEY')); | ||
} | ||
|
||
public static function upsertCustomer($email) | ||
{ | ||
global $joinBlockLog; | ||
|
||
$customers = Customer::all([ | ||
'email' => $email, | ||
'limit' => 1 // We just need the first match | ||
]); | ||
|
||
$newCustomer = false; | ||
|
||
if (count($customers->data) > 0) { | ||
$customer = $customers->data[0]; | ||
} else { | ||
$newCustomer = true; | ||
|
||
$customer = Customer::create([ | ||
'email' => $email | ||
]); | ||
|
||
$joinBlockLog->info('Customer created successfully! Customer ID: ' . $customer->id); | ||
} | ||
|
||
return [$customer, $newCustomer]; | ||
} | ||
|
||
public static function createSubscription($customer, $plan) | ||
{ | ||
$subscription = Subscription::create([ | ||
'customer' => $customer->id, | ||
'items' => [ | ||
[ | ||
'price' => $plan['stripe_price_id'], | ||
], | ||
], | ||
'payment_behavior'=> 'default_incomplete', | ||
'payment_settings' => ['save_default_payment_method' => 'on_subscription'], | ||
'expand' => ['latest_invoice.payment_intent'], | ||
]); | ||
|
||
return $subscription; | ||
} | ||
|
||
public static function confirmSubscriptionPaymentIntent($subscription, $confirmationTokenId) | ||
{ | ||
global $joinBlockLog; | ||
|
||
$joinBlockLog->info('Confirming payment intent for subscription', $subscription->toArray()); | ||
|
||
if (!$subscription->latest_invoice || !$subscription->latest_invoice->payment_intent) { | ||
$joinBlockLog->info('No payment intent found for this subscription. It might be a free trial or zero-amount invoice'); | ||
return null; | ||
} | ||
|
||
$paymentIntentId = $subscription->latest_invoice->payment_intent->id; | ||
$paymentIntent = \Stripe\PaymentIntent::retrieve($paymentIntentId); | ||
|
||
$confirmedPaymentIntent = $paymentIntent->confirm([ | ||
'confirmation_token' => $confirmationTokenId, | ||
]); | ||
|
||
return $confirmedPaymentIntent; | ||
} | ||
|
||
public static function updateCustomerDefaultPaymentMethod($customerId, $paymentMethodId) | ||
{ | ||
Customer::update( | ||
$customerId, | ||
[ | ||
'invoice_settings' => [ | ||
'default_payment_method' => $paymentMethodId, | ||
], | ||
] | ||
); | ||
} | ||
|
||
public static function convertFrequencyToStripeInterval($frequency) | ||
{ | ||
switch ($frequency) { | ||
case 'monthly': | ||
return 'month'; | ||
case 'yearly': | ||
return 'year'; | ||
case 'weekly': | ||
return 'week'; | ||
case 'daily': | ||
return 'day'; | ||
} | ||
} | ||
|
||
public static function createMembershipPlanIfItDoesNotExist($membershipPlan) | ||
{ | ||
global $joinBlockLog; | ||
|
||
$newOrExistingProduct = self::getOrCreateProductForMembershipTier($membershipPlan); | ||
$newOrExistingPrice = self::getOrCreatePriceForProduct($newOrExistingProduct, $membershipPlan['amount'], $membershipPlan['currency'], self::convertFrequencyToStripeInterval($membershipPlan['frequency'])); | ||
|
||
return [$newOrExistingProduct, $newOrExistingPrice]; | ||
} | ||
|
||
public static function getOrCreateProductForMembershipTier($membershipPlan) | ||
{ | ||
global $joinBlockLog; | ||
|
||
$tierID = sanitize_title($membershipPlan['label']); | ||
|
||
$tierDescription = $membershipPlan['description']; | ||
|
||
try { | ||
$joinBlockLog->info("Searching for existing Stripe product for membership tier '{$tierID}'"); | ||
|
||
$existingProducts = \Stripe\Product::search([ | ||
'query' => "active:'true' AND metadata['membership_plan']:'{$tierID}'", | ||
]); | ||
|
||
if (count($existingProducts->data) > 0) { | ||
$existingProduct = $existingProducts->data[0]; | ||
$joinBlockLog->info("Product for membership tier '{$tierID}' already exists, with Stripe ID {$existingProduct->id}"); | ||
|
||
// Check if the product needs to be updated | ||
$needsUpdate = false; | ||
$updateData = []; | ||
|
||
if ($existingProduct->name !== "Membership: {$membershipPlan['label']}") { | ||
$joinBlockLog->info("Name changed, updating existing product for membership tier '{$tierID}'"); | ||
|
||
$updateData['name'] = "Membership: {$membershipPlan['label']}"; | ||
$needsUpdate = true; | ||
} | ||
|
||
if ($existingProduct->description !== $tierDescription) { | ||
$joinBlockLog->info("Description changed, updating existing product for membership tier '{$tierID}'"); | ||
|
||
$updateData['description'] = $tierDescription; | ||
$needsUpdate = true; | ||
} | ||
|
||
if ($needsUpdate) { | ||
$updatedProduct = \Stripe\Product::update($existingProduct->id, $updateData); | ||
$joinBlockLog->info("Product updated for membership tier '{$tierID}', with Stripe ID {$updatedProduct->id}"); | ||
return $updatedProduct; | ||
} | ||
|
||
return $existingProduct; | ||
} | ||
|
||
$joinBlockLog->info("No existing product found for membership tier '{$tierID}', creating new product"); | ||
|
||
$stripeProduct = [ | ||
'name' => "Membership: {$membershipPlan['label']}", | ||
'type' => 'service', | ||
'metadata' => ['membership_plan' => $tierID], | ||
]; | ||
|
||
if ($tierDescription) { | ||
$stripeProduct['description'] = $tierDescription; | ||
} | ||
|
||
$newProduct = \Stripe\Product::create($stripeProduct); | ||
|
||
$joinBlockLog->info("New Stripe product created for membership tier '{$tierID}'. Stripe Product ID {$newProduct->id}"); | ||
|
||
return $newProduct; | ||
} catch (\Stripe\Exception\ApiErrorException $e) { | ||
$joinBlockLog->error("Error creating/retrieving product: " . $e->getMessage()); | ||
throw $e; | ||
} | ||
} | ||
|
||
public static function getOrCreatePriceForProduct($product, $amount, $currency, $interval) | ||
{ | ||
global $joinBlockLog; | ||
|
||
// Stripe requires the price in lowest denomination of the currency. E.G. cents for USD, pence for GBP. | ||
// So we multiply the amount by 100 to get the price in this format. | ||
// We store the amount in whole units of the currency, e.g. dollars for USD, pounds for GBP. | ||
$stripePrice = $amount * 100; | ||
|
||
try { | ||
$joinBlockLog->info("Searching for existing Stripe price for recurring product '{$product->id}' with currency '{$currency}'"); | ||
|
||
$existingPrices = \Stripe\Price::search([ | ||
'query' => "active:'true' AND product:'{$product->id}' AND type:'recurring' AND currency:'{$currency}'", | ||
]); | ||
|
||
if (count($existingPrices->data) > 0) { | ||
$joinBlockLog->info("Recurring price for product '{$product->id}' with currency '{$currency}' already exists."); | ||
return $existingPrices->data[0]; | ||
} | ||
|
||
$joinBlockLog->info("No existing price found for product '{$product->id}' with currency '{$currency}', creating new price"); | ||
|
||
$newPrice = \Stripe\Price::create([ | ||
'product' => $product->id, | ||
'unit_amount' => $stripePrice, | ||
'currency' => $currency, | ||
'recurring' => ['interval' => $interval], | ||
]); | ||
|
||
$joinBlockLog->info("New Stripe price created for product '{$product->id}'. Stripe Price ID {$newPrice->id}"); | ||
|
||
return $newPrice; | ||
} catch (ApiErrorException $e) { | ||
$joinBlockLog->error("Error creating/retrieving price: " . $e->getMessage()); | ||
throw $e; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.