以正确的方式为单个用户批量支付订阅
Batch paying subscriptions for a single user the proper way
在我们的平台上,我们通过记录每个用户的订阅量、条带订阅 ID(每个用户只有一个 ID)、创建时间和结束时间来跟踪每个用户的订阅。
目前这个系统的运作方式是这样的:
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 [=10=]/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'));
}
这是在最初的创作中。所以你可以想象,随着用户不断向他们的帐户添加订阅(可以有多个),我们将其记录在我们的系统中,他们会立即为该订阅付费,然后我们等到 stripe 的下一张发票向他们收费下个月的第一天。
这来自他们自己的文档和推荐:https://support.stripe.com/questions/batch-multiple-transactions-from-one-customer-into-a-single-charge
我们目前遇到的问题是,一旦向用户收取了本月(6 月 1 日)的费用,他们的费用为 0 美元(因为订阅就是这样),但我注意到他们有发票行项目,但那些是添加下个月。
我们添加这些行项目的方式是通过 webhooks。这是它的样子:
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;
这里发生的是,我们检索 invoice.created
的条带对象,为其获取适当的数据并在我们的数据库中找到与其关联的客户数据,然后在内部检查每个订阅项目未被禁用,并且其过期日期与发票创建日期一致。
该项目已添加到发票中,但不是用于即时发票 - 它是为下一个账单月(7 月 1 日)完成的。为什么会这样?这也是正确的方法吗?有更有效的方法吗?
同样有趣的是,我们有一个订阅完全错过了下个月(6 月)的 1 日,并在 6 月 30 日开始计费(他们的订阅于 5 月 30 日在 11:52PM 开始)。它完全忽略了“本月第一天”的纪元时间。这也有原因吗?
发票项目最终出现在下一张循环发票上,因为您正在创建客户发票项目 -- 它们将保持待处理状态 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),并带有草稿发票 ID。
至于你举的例子的日期,你确定你设置的是billing_cycle_anchor
?在您共享的代码中,此参数仅在客户没有现有订阅时使用:
else if(count($subscriptions) == 0)
{
//If not, create an entirely new one for the user.
$subscription = $stripe->subscriptions->create([
...
'billing_cycle_anchor' => $firstofmonth
]);
}
在我们的平台上,我们通过记录每个用户的订阅量、条带订阅 ID(每个用户只有一个 ID)、创建时间和结束时间来跟踪每个用户的订阅。
目前这个系统的运作方式是这样的:
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 [=10=]/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'));
}
这是在最初的创作中。所以你可以想象,随着用户不断向他们的帐户添加订阅(可以有多个),我们将其记录在我们的系统中,他们会立即为该订阅付费,然后我们等到 stripe 的下一张发票向他们收费下个月的第一天。
这来自他们自己的文档和推荐:https://support.stripe.com/questions/batch-multiple-transactions-from-one-customer-into-a-single-charge
我们目前遇到的问题是,一旦向用户收取了本月(6 月 1 日)的费用,他们的费用为 0 美元(因为订阅就是这样),但我注意到他们有发票行项目,但那些是添加下个月。
我们添加这些行项目的方式是通过 webhooks。这是它的样子:
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;
这里发生的是,我们检索 invoice.created
的条带对象,为其获取适当的数据并在我们的数据库中找到与其关联的客户数据,然后在内部检查每个订阅项目未被禁用,并且其过期日期与发票创建日期一致。
该项目已添加到发票中,但不是用于即时发票 - 它是为下一个账单月(7 月 1 日)完成的。为什么会这样?这也是正确的方法吗?有更有效的方法吗?
同样有趣的是,我们有一个订阅完全错过了下个月(6 月)的 1 日,并在 6 月 30 日开始计费(他们的订阅于 5 月 30 日在 11:52PM 开始)。它完全忽略了“本月第一天”的纪元时间。这也有原因吗?
发票项目最终出现在下一张循环发票上,因为您正在创建客户发票项目 -- 它们将保持待处理状态 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),并带有草稿发票 ID。
至于你举的例子的日期,你确定你设置的是billing_cycle_anchor
?在您共享的代码中,此参数仅在客户没有现有订阅时使用:
else if(count($subscriptions) == 0)
{
//If not, create an entirely new one for the user.
$subscription = $stripe->subscriptions->create([
...
'billing_cycle_anchor' => $firstofmonth
]);
}