以正确的方式为单个用户批量支付订阅

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
    ]);
}