Reputation: 2338
On our platform, we track each user's subscriptions by logging the amount they are subscribed to, the stripe subscription ID (will only be one ID per user), when it was created and when it will end.
Currently how this system works is like so:
if($this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'))
{
return new Response($this->redirect($request->getUri()));
}
$user = $this->getUser();
if(!$user->isVerified())
{
return new Response("must be verified");
}
$token = $_POST['csrf'];
$amount = $_POST['quantity'];
$project_id = $_POST['project'];
$tier_id = $_POST['tier'];
//Create a new user subscription
$userSub = new UserSubscription();
//Get all the repos for the project, tier and users.
$projectRepo = $this->getDoctrine()->getRepository(Project::class);
$tierRepo = $this->getDoctrine()->getRepository(ProjectTier::class);
$userRepo = $this->getDoctrine()->getRepository(User::class);
//Find the right project through the project ID
$project = $projectRepo->findOneBy([
"id" => $project_id
]);
//Find the right tier through the tier ID + the project
$tier = $tierRepo->findOneBy([
"id" => $tier_id,
'project' => $project_id
]);
//Find the project owner.
$owner = $project->getProjectOwner();
//Get the owner stripe connect ID.
$owner_id = $owner->getStripeConnectId();
if(!$this->isCsrfTokenValid('subscription-form', $token))
{
return new Response("failure csrf");
}
if(!$project)
{
return new Response("failure project");
}
if(!$tier)
{
return new Response("failure tier");
}
if($owner_id == null)
{
return new Response("failure owner");
}
if(!is_numeric($amount))
{
return new Response("amount invalid");
}
if($amount < $tier->getTierPrice() || $amount < 1)
{
return new Response("amount too little");
}
//Get the stripe customer ID from the user.
$id = $user->getStripeCustomerId();
//Call the stripe API
$stripe = new \Stripe\StripeClient($this->stripeSecretKey);
//Get the user object in stripe.
$customerData = $stripe->customers->retrieve($id);
//If there is no payment source for the user, let them know.
if($customerData->default_source == null)
{
return new Response("No card");
}
//Retrieve all products -- there is only one in stripe with a $0/month payment.
$allPrices = $stripe->prices->all(['active'=>true]);
//Cycle all through them, really if there is one, it will only pick that one up and stuff it into the var.
foreach($allPrices['data'] as $item)
{
if($item->type == "recurring" && $item->billing_scheme == "per_unit" && $item->unit_amount == "0")
{
$price = $item;
break;
}
}
//Get the first of next month.
$firstofmonth = strtotime('first day of next month'); //
//$first2months = strtotime('first day of +2 months');
//Grab the customer's payment source.
$card = $stripe->customers->retrieveSource(
$id,
$customerData->default_source
);
//If its not in the US, change the stripe percent fee.
if($card->country != "US")
{
$stripePercent = 0.039;
}
else
{
//Otherwise regular percentage fee.
$stripePercent = 0.029;
}
//30 cents per transaction
$stripeCents = 30;
//Platform fee.
$platformfee = 0.025;
$chargeAmount = number_format($amount,2,"","");
$subscription_expiration = $firstofmonth;
//Calculate the full fees.
$fees = number_format(($chargeAmount*$stripePercent+$stripeCents), -2,"", "")+number_format(($chargeAmount*$platformfee), -2,"", "");
//Create a payment intent for the intial payment.
$pi = $stripe->paymentIntents->create([
'amount' => $chargeAmount,
'currency' => 'usd',
'customer' => $user->getStripeCustomerId(),
'application_fee_amount' => $fees,
'transfer_data' => [
'destination' => $owner_id
],
'payment_method' => $customerData->default_source
]);
//Confirm the payment intent.
$confirm = $stripe->paymentIntents->confirm(
$pi->id,
['payment_method' => $customerData->default_source]
);
//Get "all" the subscriptions from from the user -- its only 1
$subscriptions = $stripe->subscriptions->all(["customer" => $id]);
//If only one, then proceed.
if(count($subscriptions) == 1)
{
$subscription = $subscriptions['data'][0];
}
else if(count($subscriptions) == 0)
{
//If not, create an entirely new one for the user.
$subscription = $stripe->subscriptions->create([
'customer' => $user->getStripeCustomerId(),
'items' => [
[
'price' => $price->id //0/month subscription
],
],
'billing_cycle_anchor' => $firstofmonth
]);
}
//If the subscription is created and the payment is confirmed...
if($confirm && $subscription)
{
//Create the new subscription with the user, the project, tier, the stripe subscription ID, when it expires, when it was created and how much.
$userSub->setUser($user)
->setProject($project)
->setProjectTier($tier)
->setStripeSubscription($subscription->id)
->setExpiresOn($subscription_expiration)
->setCreatedOn(time())
->setSubscriptionAmount($amount);
//Propogate to DB.
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($userSub);
$entityManager->flush();
//Notify the user on the front end
$this->addFlash(
'success',
'You successfully subscribed to '.$project->getProjectName()
);
//Take them to their feed.
return new Response($this->generateUrl('feed'));
}
This is on the initial creation. So as you can imagine, as the user keeps adding subscriptions to their account (there can be more than one), we log it in our system, they get charged immediately for that said subscription and then we wait until stripe's next invoice to bill them collectively on the first of the next month.
This is coming from their own documentation and recommendation here: https://support.stripe.com/questions/batch-multiple-transactions-from-one-customer-into-a-single-charge
The problem we had currently is that once the users were billed for this month (the first of June) they were billed $0 (because the subscription is that) but I noticed and they have invoice line items, but those were added for the following month.
The way we add these line items are through webhooks. Here is what that looks like:
case 'invoice.created':
$stripe = new \Stripe\StripeClient($this->stripeSecretKey);
$obj = $event->data->object;
$invoice_id = $obj->id;
$sub_id = $obj->subscription;
$subRepo = $userSubscriptionRepository->findBy([
"StripeSubscription" => $sub_id
]);
$user = $userSubscriptionRepository->findOneBy([
"StripeSubscription" => $sub_id
]);
$customerID = $user->getUser()->getStripeCustomerId();
$firstofmonth = strtotime('first day of next month');
foreach($subRepo as $item)
{
if($item->getDisabled() == false && date("d/m/Y", $item->getExpiresOn()) == date("d/m/Y", $obj->created))
{
$stripe->invoiceItems->create([
'customer' => $customerID,
'amount' => $item->getSubscriptionAmount()."00",
'currency' => 'usd',
'description' => 'For project with ID: '.$item->getProject()->getId(),
'metadata' => [
'sub_id' => $item->getId()
]
]);
}
}
break;
What happens here is that we retrieve the stripe object for the invoice.created
, get the appropriate data for it and find the customer data in our DB associated to it, and then check internally for each subscription that this item isn't disabled, and that the date for it's expiration matches the date for the day that the invoice was created.
The item was added to the invoice, but not for the immediate invoice - it was done for the next billing month (1st of Jul). Why did this happen? Is this also the proper way to do this? Is there more of an efficient way of doing this?
Also interestingly enough, we had a single subscription that entirely missed the 1st of next month (June) and started billing on the 30th of June (their subscription started on the 30th of May at 11:52PM). It completely ignored the "First of the month" epoch time. Is there a reason for this too?
Upvotes: 0
Views: 121
Reputation: 7419
The invoice items end up on the next recurring invoice because you're creating customer invoice items -- they will remain pending until the next invoice is created. If you want to add items to a draft subscription invoice you need to specify the invoice
parameter (ref) with the draft Invoice id.
As for the date of the example you gave, are you sure you set the billing_cycle_anchor
? In the code you shared this parameter is only used when the customer has no existing subscriptions:
else if(count($subscriptions) == 0)
{
//If not, create an entirely new one for the user.
$subscription = $stripe->subscriptions->create([
...
'billing_cycle_anchor' => $firstofmonth
]);
}
Upvotes: 1