【发布时间】:2021-08-19 20:04:40
【问题描述】:
在我们的平台上,我们通过记录每个用户的订阅量、条带订阅 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 $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'));
}
这是在初始创建时。所以你可以想象,随着用户不断向他们的帐户添加订阅(可能不止一个),我们将其登录到我们的系统中,他们会立即为所述订阅收取费用,然后我们等到 Stripe 的下一张发票向他们收费集体在下个月的第一天。
这来自他们自己的文档和建议:https://support.stripe.com/questions/batch-multiple-transactions-from-one-customer-into-a-single-charge
我们目前遇到的问题是,一旦向用户收取了本月(6 月 1 日)的费用,他们的费用为 0 美元(因为订阅就是这样),但我注意到他们有发票行项目,但这些项目是为下个月。
我们添加这些订单项的方式是通过 webhook。看起来是这样的:
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 日)完成。为什么会这样?这也是正确的方法吗?有没有更有效的方法来做到这一点?
同样有趣的是,我们有一个订阅完全错过了下个月 1 日(6 月),并在 6 月 30 日开始计费(他们的订阅从 5 月 30 日晚上 11:52 开始)。它完全忽略了“月初”纪元时间。这也有原因吗?
【问题讨论】: