craftcms / commerce-stripe

Stripe payment gateway for Craft Commerce
https://plugins.craftcms.com/commerce-stripe
MIT License
31 stars 48 forks source link

Partial Payments #147

Closed amici-infotech closed 3 years ago

amici-infotech commented 3 years ago

I am trying to build a workaround so clients can pay deposits. I have used this branch of craft commece. (they said, they will implement it in commerce 4) https://github.com/craftcms/commerce/tree/feature/3.x-partial-payments

When I use this feature branch with Stripe intents payment, I can pay depoit, but I cant pay final amount. Got this error from stripe:

Screenshot at Mar 05 12-39-08

I think this is happening because Stripe mark this order as finished in Stripe Dashboard. Is there any way I can tell Stripe that this payment is partial?

lukeholder commented 3 years ago

Thanks, I can reproduce this issue and am looking into it now.

amici-infotech commented 3 years ago

Hey @lukeholder, I needed this for client so what I did is, turn off unique contrain for orderId,gatewayId,customerId. image

Then, I have extended payments intents class in my custom plugin and commented this line. image

Unfortunately, I will not be able to generate proper refund with this as it generates 2 transactions in table. Any solution on this will be much appreciated :)

nickcobley commented 3 years ago

Any update on this one?

amici-infotech commented 3 years ago

Hey @lukeholder Its been forever. Craft Commerce's native partial payment feature is useless for me without this as I always use Stripe payment gateway. Any updates on this would be much appreciated. Thanks :)

(Just making sure this is not a dead thread!!)

ivarsbariss-solspace commented 3 years ago

Hi @lukeholder, I can still recreate this problem. Do you have any update on when this might be fixed?

Craft Commerce 3.3.2 Stripe for Craft Commerce 2.3.2.1

Is there any suggested workaround since because of this Stripe Gateway currently cannot support partial payments?

amici-infotech commented 3 years ago

@ivarsbariss-solspace I make it work with a custom plugin.

First of all, I have created a module and register a new Gateway. (that file just represent Actual Payment Gateway)

use craft\commerce\services\Gateways;
use craft\events\RegisterComponentTypesEvent;
use modules\mycustomplugin\gateways\PaymentIntents;
Event::on(Gateways::class, Gateways::EVENT_REGISTER_GATEWAY_TYPES, function(RegisterComponentTypesEvent $event) {
    $event->types[] = PaymentIntents::class;
});

my PaymentIntents.php file looks like this:

<?php
namespace modules\mycustommodule\gateways;
use craft\commerce\stripe\gateways\PaymentIntents as PaymentIntentsMain;
use modules\mycustommodule\MyCustomModule;

use Craft;

use craft\commerce\base\RequestResponseInterface;
use craft\commerce\models\payments\BasePaymentForm;
use craft\commerce\models\Transaction;
use craft\commerce\stripe\models\PaymentIntent as PaymentIntentModel;
use craft\commerce\stripe\Plugin as StripePlugin;

use craft\helpers\UrlHelper;
use Stripe\PaymentIntent;

// Extending Stripe's PaymentIntent Gateway so we dont have to copy all the functions but only one which we will change code into.
class PaymentIntents extends PaymentIntentsMain
{
    /**
     * @inheritdoc
     */
    public static function displayName(): string
    {
        return Craft::t('mycustommodule', 'Custom Module - Stripe Payment Intents');
    }

    /**
     * @inheritdoc
     */
    protected function authorizeOrPurchase(Transaction $transaction, BasePaymentForm $form, bool $capture = true): RequestResponseInterface
    {

        // Custom Deposit code added by Mufi (Changing payment amount from full to deposit only, this was the goal to create this file.)
        $deposit = MyCustomModule::$instance->DepositCalculations->calc($transaction->getOrder());

        if($deposit > 0)
        {
            $transaction->amount = $deposit;
            $transaction->paymentAmount = $deposit;
        }

        $this->configureStripeClient();
        /** @var PaymentForm $form */
        $requestData = $this->buildRequestData($transaction);
        $paymentMethodId = $form->paymentMethodId;

        $customer = null;
        $paymentIntent = null;

        $stripePlugin = StripePlugin::getInstance();

        if ($form->customer) {
            $requestData['customer'] = $form->customer;
            $customer = $stripePlugin->getCustomers()->getCustomerByReference($form->customer);
        } else if ($user = $transaction->getOrder()->getUser()) {
            $customer = $stripePlugin->getCustomers()->getCustomer($this->id, $user);
            $requestData['customer'] = $customer->reference;
        }

        $requestData['payment_method'] = $paymentMethodId;

        try {
            // If this is a customer that's logged in, attempt to continue the timeline
            if ($customer) {
                $paymentIntentService = $stripePlugin->getPaymentIntents();

                // Commented by Mufi
                // $paymentIntent = $paymentIntentService->getPaymentIntent($this->id, $transaction->orderId, $customer->id);
            }

            // If a payment intent exists, update that.
            if ($paymentIntent) {
                $stripePaymentIntent = PaymentIntent::update($paymentIntent->reference, $requestData, ['idempotency_key' => $transaction->hash]);
            } else {
                $requestData['capture_method'] = $capture ? 'automatic' : 'manual';
                $requestData['confirmation_method'] = 'manual';
                $requestData['confirm'] = false;

                $stripePaymentIntent = PaymentIntent::create($requestData, ['idempotency_key' => $transaction->hash]);

                if ($customer) {
                    $paymentIntent = new PaymentIntentModel([
                        'orderId' => $transaction->orderId,
                        'customerId' => $customer->id,
                        'gatewayId' => $this->id,
                        'reference' => $stripePaymentIntent->id,
                    ]);
                }
            }

            if ($paymentIntent) {
                // Save data before confirming.
                $paymentIntent->intentData = $stripePaymentIntent->jsonSerialize();
                $paymentIntentService->savePaymentIntent($paymentIntent);
            }

            $this->_confirmPaymentIntent($stripePaymentIntent, $transaction);

            return $this->createPaymentResponseFromApiResource($stripePaymentIntent);
        } catch (\Exception $exception) {
            return $this->createPaymentResponseFromError($exception);
        }
    }

    /**
     * Confirm a payment intent and set the return URL.
     *
     * @param PaymentIntent $stripePaymentIntent
     */
    private function _confirmPaymentIntent(PaymentIntent $stripePaymentIntent, Transaction $transaction)
    {
        $this->configureStripeClient();
        $stripePaymentIntent->confirm([
            'return_url' => UrlHelper::actionUrl('commerce/payments/complete-payment', ['commerceTransactionId' => $transaction->id, 'commerceTransactionHash' => $transaction->hash])
        ]);
    }
}

The idea was to extend actual payment gateway and modify the authorizeOrPurchase function to add Deposit and comment out the line where paymentIntents goes to check for old row into database because that part was throwing error.

Hope this helps.

lukeholder commented 3 years ago

@amici-infotech @nickcobley @ivarsbariss-solspace Fix is in PR #181

You are welcome to test beforehand, but will be in the next release.

ivarsbariss-solspace commented 3 years ago

@lukeholder Thank you! Do you have an approximate timeline for the next release?

ivarsbariss-solspace commented 3 years ago

@lukeholder I tried to test the PR #181 but after installing the fix branch's version and running the migrations I got this:

Database Exception: SQLSTATE[HY000]: General error: 1553 Cannot drop index 'idx_kakionsisopbqabdirzzlztqrqhtbiwphmac': needed in a foreign key constraint
The SQL being executed was: DROP INDEX idx_kakionsisopbqabdirzzlztqrqhtbiwphmac ON stripe_paymentintents

Migration: craft\commerce\stripe\migrations\m210903_040320_payment_intent_unique_on_transaction

Output:

> add column transactionHash string AFTER orderId to table {{%stripe_paymentintents}} ... done (time: 0.082s)
> drop index idx_kakionsisopbqabdirzzlztqrqhtbiwphmac on {{%stripe_paymentintents}} ...Exception: SQLSTATE[HY000]: General error: 1553 Cannot drop index 'idx_kakionsisopbqabdirzzlztqrqhtbiwphmac': needed in a foreign key constraint
The SQL being executed was: DROP INDEX idx_kakionsisopbqabdirzzlztqrqhtbiwphmac ON stripe_paymentintents (/Users/ivarsbariss/Web/craft-3/vendor/yiisoft/yii2/db/Schema.php:678)
#0 /Users/ivarsbariss/Web/craft-3/vendor/yiisoft/yii2/db/Command.php(1304): yii\db\Schema->convertException(Object(PDOException), 'DROP INDEX idx...')
#1 /Users/ivarsbariss/Web/craft-3/vendor/yiisoft/yii2/db/Command.php(1099): yii\db\Command->internalExecute('DROP INDEX idx...')
#2 /Users/ivarsbariss/Web/craft-3/vendor/yiisoft/yii2/db/Migration.php(507): yii\db\Command->execute()
#3 /Users/ivarsbariss/Web/craft-3/vendor/craftcms/cms/src/helpers/MigrationHelper.php(754): yii\db\Migration->dropIndex('idx_kakionsisop...', '{{%stripe_payme...')
#4 /Users/ivarsbariss/Web/craft-3/vendor/craftcms/cms/src/helpers/MigrationHelper.php(580): craft\helpers\MigrationHelper::_dropIndex('{{%stripe_payme...', 'idx_kakionsisop...', Object(craft\commerce\stripe\migrations\m210903_040320_payment_intent_unique_on_transaction))
#5 /Users/ivarsbariss/Web/craft-3/plugins/commerce-stripe-feature-fix-147/src/migrations/m210903_040320_payment_intent_unique_on_transaction.php(26): craft\helpers\MigrationHelper::dropAllIndexesOnTable('{{%stripe_payme...', Object(craft\commerce\stripe\migrations\m210903_040320_payment_intent_unique_on_transaction))
#6 /Users/ivarsbariss/Web/craft-3/vendor/craftcms/cms/src/db/Migration.php(52): craft\commerce\stripe\migrations\m210903_040320_payment_intent_unique_on_transaction->safeUp()
#7 /Users/ivarsbariss/Web/craft-3/vendor/craftcms/cms/src/db/MigrationManager.php(232): craft\db\Migration->up(true)
#8 /Users/ivarsbariss/Web/craft-3/vendor/craftcms/cms/src/db/MigrationManager.php(148): craft\db\MigrationManager->migrateUp(Object(craft\commerce\stripe\migrations\m210903_040320_payment_intent_unique_on_transaction))
#9 /Users/ivarsbariss/Web/craft-3/vendor/craftcms/cms/src/services/Updates.php(257): craft\db\MigrationManager->up()
#10 /Users/ivarsbariss/Web/craft-3/vendor/craftcms/cms/src/controllers/BaseUpdaterController.php(509): craft\services\Updates->runMigrations(Array)
#11 /Users/ivarsbariss/Web/craft-3/vendor/craftcms/cms/src/controllers/UpdaterController.php(203): craft\controllers\BaseUpdaterController->runMigrations(Array, 'restore-db')
#12 [internal function]: craft\controllers\UpdaterController->actionMigrate()
#13 /Users/ivarsbariss/Web/craft-3/vendor/yiisoft/yii2/base/InlineAction.php(57): call_user_func_array(Array, Array)
#14 /Users/ivarsbariss/Web/craft-3/vendor/yiisoft/yii2/base/Controller.php(181): yii\base\InlineAction->runWithParams(Array)
#15 /Users/ivarsbariss/Web/craft-3/vendor/craftcms/cms/src/web/Controller.php(190): yii\base\Controller->runAction('migrate', Array)
#16 /Users/ivarsbariss/Web/craft-3/vendor/yiisoft/yii2/base/Module.php(534): craft\web\Controller->runAction('migrate', Array)
#17 /Users/ivarsbariss/Web/craft-3/vendor/craftcms/cms/src/web/Application.php(274): yii\base\Module->runAction('updater/migrate', Array)
#18 /Users/ivarsbariss/Web/craft-3/vendor/craftcms/cms/src/web/Application.php(665): craft\web\Application->runAction('updater/migrate')
#19 /Users/ivarsbariss/Web/craft-3/vendor/craftcms/cms/src/web/Application.php(232): craft\web\Application->_processUpdateLogic(Object(craft\web\Request))
#20 /Users/ivarsbariss/Web/craft-3/vendor/yiisoft/yii2/base/Application.php(392): craft\web\Application->handleRequest(Object(craft\web\Request))
#21 /Users/ivarsbariss/Web/craft-3/public/index.php(25): yii\base\Application->run()
#22 {main}
ivarsbariss-solspace commented 3 years ago

Hi @lukeholder, any update on this fix's bug and the timeline for the release?

ivarsbariss-solspace commented 3 years ago

@lukeholder if this helps I was able to run the migration by adding this line to your migration before dropping the unique indexes: image

MigrationHelper::dropAllForeignKeysOnTable('{{%stripe_paymentintents}}', $this);

I bet you need to put back correctly the foreign keys at the end of the migration just like you did with the Unique indexes.

apitel commented 3 years ago

@lukeholder Can you please advise if Craft plans to address this bug? We selected Craft Commerce for a major e-commerce project ~ 6 months ago with Partial Payments being a major component of that selection, and are awaiting a fix on this before we can launch the new site.

lukeholder commented 3 years ago

@apitel @ivarsbariss-solspace Sorry about this. I was monitoring the PR #181 for feedback and missed this issue feedback. I have fixed the FK error.

Please add feedback to the PR and I will merge and update Stripe today.