Skip to content

Commit

Permalink
Merge pull request #45 from commonknowledge/feature/ck-3811-core-supp…
Browse files Browse the repository at this point in the history
…ort-for-stripe-in-minimalist-join-form

CK-3811 Core Support for Stripe in Minimalist Join Flow
  • Loading branch information
conatus authored Oct 9, 2024
2 parents 614e936 + f92cbef commit cf990a2
Show file tree
Hide file tree
Showing 5 changed files with 414 additions and 198 deletions.
82 changes: 21 additions & 61 deletions packages/join-block/join.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
];
}
));
Expand Down
226 changes: 226 additions & 0 deletions packages/join-block/src/Services/StripeService.php
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;
}
}
}
34 changes: 34 additions & 0 deletions packages/join-block/src/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
use Carbon_Fields\Field\Html_Field;
use Carbon_Fields\Field\Select_Field;

use CommonKnowledge\JoinBlock\Services\StripeService;

const CONTAINER_ID = 'ck_join_flow';

class Settings
Expand Down Expand Up @@ -158,6 +160,36 @@ public static function init()
Settings::saveMembershipPlans($membership_plans);
}
}, 10, 2);

add_action('ck_join_flow_membership_plan_saved', function($membershipPlan) {
global $joinBlockLog;

if (!Settings::get('USE_STRIPE')) {
return;
}

$joinBlockLog->info('Creating or retrieving membership plan in Stripe', $membershipPlan);

StripeService::initialise();
[$newOrExistingProduct, $newOrExistingPrice] = StripeService::createMembershipPlanIfItDoesNotExist($membershipPlan);

$joinBlockLog->info('Membership plan created or retrieved from Stripe', [
'product' => $newOrExistingProduct->id,
'price' => $newOrExistingPrice->id,
]);

$membershipPlan['stripe_product_id'] = $newOrExistingProduct->id;
$membershipPlan['stripe_price_id'] = $newOrExistingPrice->id;

$membershipPlanID = sanitize_title($membershipPlan['label']);
update_option('ck_join_flow_membership_plan_' . $membershipPlanID, $membershipPlan);

$joinBlockLog->info("Membership plan {$membershipPlanID} saved");

$joinBlockLog->info('Membership plan retrieved from options', self::getMembershipPlan($membershipPlanID));

wp_cache_delete('ck_join_flow_membership_plan_' . $membershipPlanID, 'options');
});
}

public static function createMembershipPlansField($name = 'membership_plans')
Expand Down Expand Up @@ -242,6 +274,8 @@ public static function saveMembershipPlans($membership_plans)
{
foreach ($membership_plans as $plan) {
update_option('ck_join_flow_membership_plan_' . sanitize_title($plan['label']), $plan);

do_action('ck_join_flow_membership_plan_saved', $plan);
}
}

Expand Down
Loading

0 comments on commit cf990a2

Please sign in to comment.