Fix: Stripping Lunar's Stripe Driver with Boboko's Stripe Payment Driver
This commit is contained in:
+1
-1
@@ -18,7 +18,7 @@
|
|||||||
"lunarphp/search": "*",
|
"lunarphp/search": "*",
|
||||||
"lunarphp/meilisearch": "*",
|
"lunarphp/meilisearch": "*",
|
||||||
"spatie/laravel-translation-loader": "^2.8",
|
"spatie/laravel-translation-loader": "^2.8",
|
||||||
"lunarphp/stripe": "^1.5"
|
"stripe/stripe-php": "^16.6"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Lunar\Base\Migration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* First-party copy of lunarphp/stripe's own create_stripe_payment_intents_table
|
||||||
|
* migration (package removed in favour of depending on stripe/stripe-php
|
||||||
|
* directly — see Modules\Core\Payment\Support\StripeManager and
|
||||||
|
* Modules\Core\Payment\Models\StripePaymentIntent, which replace the
|
||||||
|
* package's own classes over this same table). Timestamped to run just
|
||||||
|
* before this app's own add_context_to_stripe_payment_intents migration,
|
||||||
|
* which already alters this table.
|
||||||
|
*/
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create($this->prefix.'stripe_payment_intents', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('cart_id')->constrained($this->prefix.'carts');
|
||||||
|
$table->foreignId('order_id')->nullable()->constrained($this->prefix.'orders');
|
||||||
|
$table->string('intent_id')->index();
|
||||||
|
$table->string('status')->nullable();
|
||||||
|
$table->string('event_id')->index()->nullable();
|
||||||
|
$table->timestamp('processing_at')->nullable();
|
||||||
|
$table->timestamp('processed_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists($this->prefix.'stripe_payment_intents');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -4,9 +4,6 @@ namespace Modules\Core\Payment\Drivers;
|
|||||||
|
|
||||||
use Lunar\DataTypes\Price;
|
use Lunar\DataTypes\Price;
|
||||||
use Lunar\Models\Currency;
|
use Lunar\Models\Currency;
|
||||||
use Lunar\Stripe\Facades\Stripe;
|
|
||||||
use Lunar\Stripe\Managers\StripeManager;
|
|
||||||
use Lunar\Stripe\Models\StripePaymentIntent;
|
|
||||||
use Modules\Core\Payment\Contracts\Configurable;
|
use Modules\Core\Payment\Contracts\Configurable;
|
||||||
use Modules\Core\Payment\Contracts\HandlesPaymentCallback;
|
use Modules\Core\Payment\Contracts\HandlesPaymentCallback;
|
||||||
use Modules\Core\Payment\Contracts\SupportsAuthorization;
|
use Modules\Core\Payment\Contracts\SupportsAuthorization;
|
||||||
@@ -26,17 +23,20 @@ use Modules\Core\Payment\Events\PaymentRefundFailed;
|
|||||||
use Modules\Core\Payment\Events\PaymentRefunded;
|
use Modules\Core\Payment\Events\PaymentRefunded;
|
||||||
use Modules\Core\Payment\Events\PaymentVoidFailed;
|
use Modules\Core\Payment\Events\PaymentVoidFailed;
|
||||||
use Modules\Core\Payment\Events\PaymentVoided;
|
use Modules\Core\Payment\Events\PaymentVoided;
|
||||||
|
use Modules\Core\Payment\Models\StripePaymentIntent;
|
||||||
|
use Modules\Core\Payment\Support\StripeManager;
|
||||||
use Stripe\Exception\ApiErrorException;
|
use Stripe\Exception\ApiErrorException;
|
||||||
use Stripe\PaymentIntent;
|
use Stripe\PaymentIntent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Talks to Stripe's PaymentIntent API directly — deliberately NOT via
|
* Talks to Stripe's PaymentIntent API directly — deliberately NOT via
|
||||||
* Lunar\Stripe\Facades\Stripe::createIntent()/fetchOrCreateIntent(), which
|
* Lunar's own checkout flow (lunarphp/stripe, since removed — see
|
||||||
* take a Lunar\Models\Cart and derive amount/currency from it. Payment
|
* Modules\Core\Payment\Support\StripeManager's own docblock), which took a
|
||||||
* must never receive a Cart (see docs/payments.md) — pay()/authorize()
|
* Lunar\Models\Cart and derived amount/currency from it. Payment must
|
||||||
* already receive $amount explicitly as their own required Lunar Price
|
* never receive a Cart (see docs/payments.md) — pay()/authorize() already
|
||||||
* parameter (see PaymentResult's own docblock), the caller's job to
|
* receive $amount explicitly as their own required Lunar Price parameter
|
||||||
* assemble, same as every other driver.
|
* (see PaymentResult's own docblock), the caller's job to assemble, same
|
||||||
|
* as every other driver.
|
||||||
*
|
*
|
||||||
* Every amount that crosses this class's own boundary is converted right
|
* Every amount that crosses this class's own boundary is converted right
|
||||||
* there: Lunar's Price -> Stripe's minor-unit int going INTO a gateway
|
* there: Lunar's Price -> Stripe's minor-unit int going INTO a gateway
|
||||||
@@ -45,12 +45,11 @@ use Stripe\PaymentIntent;
|
|||||||
* Nothing outside this class ever sees a Stripe-scaled integer.
|
* Nothing outside this class ever sees a Stripe-scaled integer.
|
||||||
*
|
*
|
||||||
* Correlating a later handleCallback() (a separate request — a webhook)
|
* Correlating a later handleCallback() (a separate request — a webhook)
|
||||||
* back to whatever $context identified this attempt is solved the same
|
* back to whatever $context identified this attempt is solved via real
|
||||||
* way lunarphp/stripe's own StripePaymentType/ProcessStripeWebhook solve
|
* cart_id/order_id columns on Modules\Core\Payment\Models\
|
||||||
* it: real cart_id/order_id columns on Lunar\Stripe\Models\
|
* StripePaymentIntent (a table this app now owns outright, already shaped
|
||||||
* StripePaymentIntent (a table already owned by lunarphp/stripe, already
|
* for exactly this), not a generic context blob. See docs/payments.md
|
||||||
* shaped for exactly this), not a generic context blob. See
|
* "Async resolution" for the full reasoning.
|
||||||
* docs/payments.md "Async resolution" for the full reasoning.
|
|
||||||
*/
|
*/
|
||||||
class StripePaymentDriver implements
|
class StripePaymentDriver implements
|
||||||
Configurable,
|
Configurable,
|
||||||
@@ -61,10 +60,13 @@ class StripePaymentDriver implements
|
|||||||
SupportsRefunds,
|
SupportsRefunds,
|
||||||
HandlesPaymentCallback
|
HandlesPaymentCallback
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly StripeManager $stripe,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same key lunarphp/stripe's own StripeManager reads its API key from
|
* Same key StripeManager reads its API key from — no key, no usable
|
||||||
* (Stripe::setApiKey(config('services.stripe.key'))) — no key, no
|
* driver.
|
||||||
* usable driver.
|
|
||||||
*/
|
*/
|
||||||
public function isConfigured(): bool
|
public function isConfigured(): bool
|
||||||
{
|
{
|
||||||
@@ -114,7 +116,7 @@ class StripePaymentDriver implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$paymentIntent = Stripe::getClient()->paymentIntents->create($params);
|
$paymentIntent = $this->stripe->getClient()->paymentIntents->create($params);
|
||||||
} catch (ApiErrorException $e) {
|
} catch (ApiErrorException $e) {
|
||||||
return $this->declined($type, $amount, $e, $context, authorizing: $captureMethod === 'manual');
|
return $this->declined($type, $amount, $e, $context, authorizing: $captureMethod === 'manual');
|
||||||
}
|
}
|
||||||
@@ -128,7 +130,7 @@ class StripePaymentDriver implements
|
|||||||
{
|
{
|
||||||
[$intentModel, $type, $context] = $this->resolveIntentModel($reference, $context, $data['type'] ?? '');
|
[$intentModel, $type, $context] = $this->resolveIntentModel($reference, $context, $data['type'] ?? '');
|
||||||
|
|
||||||
$paymentIntent = Stripe::getClient()->paymentIntents->retrieve($reference);
|
$paymentIntent = $this->stripe->getClient()->paymentIntents->retrieve($reference);
|
||||||
|
|
||||||
$authorizing = $paymentIntent->capture_method === PaymentIntent::CAPTURE_METHOD_MANUAL;
|
$authorizing = $paymentIntent->capture_method === PaymentIntent::CAPTURE_METHOD_MANUAL;
|
||||||
|
|
||||||
@@ -136,7 +138,7 @@ class StripePaymentDriver implements
|
|||||||
// automatic capture_method, but Stripe stopped short of
|
// automatic capture_method, but Stripe stopped short of
|
||||||
// capturing (rare, but the API contract allows it) — finish
|
// capturing (rare, but the API contract allows it) — finish
|
||||||
// the job pay() started.
|
// the job pay() started.
|
||||||
$paymentIntent = Stripe::getClient()->paymentIntents->capture($reference);
|
$paymentIntent = $this->stripe->getClient()->paymentIntents->capture($reference);
|
||||||
}
|
}
|
||||||
|
|
||||||
$intentModel?->update(['status' => $paymentIntent->status]);
|
$intentModel?->update(['status' => $paymentIntent->status]);
|
||||||
@@ -151,7 +153,7 @@ class StripePaymentDriver implements
|
|||||||
[$intentModel, $type, $context] = $this->resolveIntentModel($reference, $context);
|
[$intentModel, $type, $context] = $this->resolveIntentModel($reference, $context);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$paymentIntent = Stripe::getClient()->paymentIntents->capture($reference, [
|
$paymentIntent = $this->stripe->getClient()->paymentIntents->capture($reference, [
|
||||||
'amount_to_capture' => StripeManager::toStripeAmount($amount->value, $amount->currency),
|
'amount_to_capture' => StripeManager::toStripeAmount($amount->value, $amount->currency),
|
||||||
]);
|
]);
|
||||||
} catch (ApiErrorException $e) {
|
} catch (ApiErrorException $e) {
|
||||||
@@ -185,7 +187,7 @@ class StripePaymentDriver implements
|
|||||||
[$intentModel, $type, $context] = $this->resolveIntentModel($reference, $context);
|
[$intentModel, $type, $context] = $this->resolveIntentModel($reference, $context);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$paymentIntent = Stripe::getClient()->paymentIntents->cancel($reference);
|
$paymentIntent = $this->stripe->getClient()->paymentIntents->cancel($reference);
|
||||||
} catch (ApiErrorException $e) {
|
} catch (ApiErrorException $e) {
|
||||||
$result = $this->failure($amount, $e, $reference);
|
$result = $this->failure($amount, $e, $reference);
|
||||||
PaymentVoidFailed::dispatch($type, $result, $context);
|
PaymentVoidFailed::dispatch($type, $result, $context);
|
||||||
@@ -216,7 +218,7 @@ class StripePaymentDriver implements
|
|||||||
[$intentModel, $type, $context] = $this->resolveIntentModel($reference, $context);
|
[$intentModel, $type, $context] = $this->resolveIntentModel($reference, $context);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$refund = Stripe::getClient()->refunds->create([
|
$refund = $this->stripe->getClient()->refunds->create([
|
||||||
'payment_intent' => $reference,
|
'payment_intent' => $reference,
|
||||||
'amount' => StripeManager::toStripeAmount($amount->value, $amount->currency),
|
'amount' => StripeManager::toStripeAmount($amount->value, $amount->currency),
|
||||||
]);
|
]);
|
||||||
@@ -253,7 +255,7 @@ class StripePaymentDriver implements
|
|||||||
'order_id' => $context['order_id'] ?? null,
|
'order_id' => $context['order_id'] ?? null,
|
||||||
'status' => $paymentIntent->status,
|
'status' => $paymentIntent->status,
|
||||||
'payment_type' => $type,
|
'payment_type' => $type,
|
||||||
'context' => json_encode($context),
|
'context' => $context,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,28 +280,10 @@ class StripePaymentDriver implements
|
|||||||
return [
|
return [
|
||||||
$intentModel,
|
$intentModel,
|
||||||
$intentModel?->payment_type ?? $typeFallback,
|
$intentModel?->payment_type ?? $typeFallback,
|
||||||
$this->decodeContext($intentModel) ?? $context,
|
$intentModel?->context ?? $context,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* StripePaymentIntent is a vendor model (lunarphp/stripe) with no cast
|
|
||||||
* declared for our own 'context' column (added by boboko-core's own
|
|
||||||
* migration, see database/migrations/..._add_context_to_stripe_
|
|
||||||
* payment_intents.php) — we can't edit the vendor model to add one, so
|
|
||||||
* decode manually here instead of assuming Eloquent already did it.
|
|
||||||
*
|
|
||||||
* @return array<string, mixed>|null
|
|
||||||
*/
|
|
||||||
private function decodeContext(?StripePaymentIntent $intentModel): ?array
|
|
||||||
{
|
|
||||||
if (! $intentModel || ! $intentModel->context) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return json_decode($intentModel->context, associative: true) ?: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a live Stripe PaymentIntent's own amount/currency back
|
* Converts a live Stripe PaymentIntent's own amount/currency back
|
||||||
* into Lunar's Price — the one place this class reads a Stripe
|
* into Lunar's Price — the one place this class reads a Stripe
|
||||||
@@ -383,7 +367,7 @@ class StripePaymentDriver implements
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$charge = Stripe::getCharge(is_string($chargeId) ? $chargeId : $chargeId->id);
|
$charge = $this->stripe->getCharge(is_string($chargeId) ? $chargeId : $chargeId->id);
|
||||||
|
|
||||||
$paymentType = collect($charge->payment_method_details)->keys()->first();
|
$paymentType = collect($charge->payment_method_details)->keys()->first();
|
||||||
$details = collect($charge->payment_method_details)->first();
|
$details = collect($charge->payment_method_details)->first();
|
||||||
|
|||||||
@@ -9,18 +9,17 @@ use Modules\Core\Payment\Drivers\StripePaymentDriver;
|
|||||||
use Stripe\Webhook;
|
use Stripe\Webhook;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A boboko-owned webhook endpoint for Stripe — deliberately NOT
|
* A boboko-owned webhook endpoint for Stripe — never went through Lunar's
|
||||||
* lunarphp/stripe's own route (vendor/lunarphp/stripe/routes/webhooks.php),
|
* own Payments::driver('stripe') flow (the flow StripePaymentDriver was
|
||||||
* which dispatches into Lunar's own Payments::driver('stripe') flow (the
|
* built to replace, see that class's own docblock), and lunarphp/stripe
|
||||||
* flow StripePaymentDriver was built to replace, see that class's own
|
* has since been removed entirely (see Modules\Core\Payment\Support\
|
||||||
* docblock). Signature verification is handled by
|
* StripeManager's own docblock). Signature verification is handled by
|
||||||
* Lunar\Stripe\Http\Middleware\StripeWebhookMiddleware, registered on this
|
* Modules\Core\Payment\Http\Middleware\StripeWebhookMiddleware, registered
|
||||||
* route (see src/Payment/routes/webhooks.php) — pure Stripe SDK
|
* on this route (see src/Payment/routes/webhooks.php) — pure Stripe SDK
|
||||||
* verification + event-type filtering, safe to reuse even though this
|
* verification + event-type filtering. This controller verifies the
|
||||||
* controller never touches the rest of that vendor package's flow. This
|
* signature again itself (Webhook::constructEvent()) to get the
|
||||||
* controller verifies the signature again itself (Webhook::constructEvent())
|
* constructed Event object — the middleware doesn't stash one anywhere
|
||||||
* to get the constructed Event object — the middleware doesn't stash one
|
* reusable, it only gates the request through.
|
||||||
* anywhere reusable, it only gates the request through.
|
|
||||||
*
|
*
|
||||||
* Resolves the driver directly by class, not via
|
* Resolves the driver directly by class, not via
|
||||||
* Modules\Core\Payment\Services\PaymentDriverRegistry — this endpoint is
|
* Modules\Core\Payment\Services\PaymentDriverRegistry — this endpoint is
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\Payment\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Stripe\Exception\SignatureVerificationException;
|
||||||
|
use Stripe\Exception\UnexpectedValueException;
|
||||||
|
use Stripe\Webhook;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* First-party replacement for Lunar\Stripe\Http\Middleware\
|
||||||
|
* StripeWebhookMiddleware (lunarphp/stripe removed — see
|
||||||
|
* Modules\Core\Payment\Support\StripeManager's own docblock). Registered
|
||||||
|
* on the same route as before (src/Payment/routes/webhooks.php) purely to
|
||||||
|
* gate malformed/irrelevant requests before they reach
|
||||||
|
* Modules\Core\Payment\Http\Controllers\StripeWebhookController, which
|
||||||
|
* re-verifies the signature itself (see that controller's own docblock)
|
||||||
|
* to get the constructed Event object — this duplication predates the
|
||||||
|
* package removal and is left unchanged here.
|
||||||
|
*/
|
||||||
|
class StripeWebhookMiddleware
|
||||||
|
{
|
||||||
|
public function handle(Request $request, ?Closure $next = null)
|
||||||
|
{
|
||||||
|
$secret = config('services.stripe.webhooks.lunar');
|
||||||
|
$stripeSig = $request->header('Stripe-Signature');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$event = Webhook::constructEvent(
|
||||||
|
$request->getContent(),
|
||||||
|
$stripeSig,
|
||||||
|
$secret
|
||||||
|
);
|
||||||
|
} catch (UnexpectedValueException|SignatureVerificationException $e) {
|
||||||
|
abort(400, $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! in_array(
|
||||||
|
$event->type,
|
||||||
|
[
|
||||||
|
'payment_intent.payment_failed',
|
||||||
|
'payment_intent.succeeded',
|
||||||
|
]
|
||||||
|
)) {
|
||||||
|
return response('', 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\Payment\Models;
|
||||||
|
|
||||||
|
use Lunar\Base\BaseModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* First-party replacement for Lunar\Stripe\Models\StripePaymentIntent (the
|
||||||
|
* lunarphp/stripe package was removed — see Modules\Core\Payment\Support\
|
||||||
|
* StripeManager's own docblock). Same table (lunar_stripe_payment_intents,
|
||||||
|
* created by database/migrations/..._create_stripe_payment_intents_table,
|
||||||
|
* a first-party copy of the vendor migration), including the app-owned
|
||||||
|
* `context`/`payment_type` columns Modules\Core\Payment\Drivers\
|
||||||
|
* StripePaymentDriver::handleCallback() needs to recover $context/$type
|
||||||
|
* across the separate request a webhook arrives on — see that class's own
|
||||||
|
* docblock for "Async resolution".
|
||||||
|
*
|
||||||
|
* Extends Lunar\Base\BaseModel (from lunarphp/core, unaffected by removing
|
||||||
|
* lunarphp/stripe) purely so table-prefix resolution
|
||||||
|
* (config('lunar.database.table_prefix')) stays identical to how the
|
||||||
|
* vendor model resolved it — this table was created under that prefix.
|
||||||
|
*/
|
||||||
|
class StripePaymentIntent extends BaseModel
|
||||||
|
{
|
||||||
|
protected $table = 'stripe_payment_intents';
|
||||||
|
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'context' => 'array',
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\Payment\Support;
|
||||||
|
|
||||||
|
use Lunar\Models\Contracts\Currency as CurrencyContract;
|
||||||
|
use Stripe\Charge;
|
||||||
|
use Stripe\StripeClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* First-party replacement for Lunar\Stripe\Facades\Stripe +
|
||||||
|
* Lunar\Stripe\Managers\StripeManager — lunarphp/stripe was removed once
|
||||||
|
* Modules\Core\Payment\Drivers\StripePaymentDriver already replaced every
|
||||||
|
* bit of Lunar's own Stripe payment flow (see that class's own docblock);
|
||||||
|
* all that remained load-bearing from the package was raw API-client
|
||||||
|
* access and amount conversion, neither of which is Lunar-specific. Only
|
||||||
|
* the methods StripePaymentDriver actually called are kept — no
|
||||||
|
* fetchOrCreateIntent()/cart-bound helpers, which belonged to Lunar's own
|
||||||
|
* (unused) checkout flow.
|
||||||
|
*
|
||||||
|
* getClient()/getCharge() call the Stripe SDK directly rather than going
|
||||||
|
* through a facade — StripePaymentDriver resolves this class via the
|
||||||
|
* container instead, same as every other dependency it takes.
|
||||||
|
*/
|
||||||
|
class StripeManager
|
||||||
|
{
|
||||||
|
public function getClient(): StripeClient
|
||||||
|
{
|
||||||
|
return new StripeClient([
|
||||||
|
'api_key' => config('services.stripe.key'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCharge(string $chargeId): Charge
|
||||||
|
{
|
||||||
|
return $this->getClient()->charges->retrieve($chargeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zero-decimal currencies, per Stripe. The amount sent to Stripe is the
|
||||||
|
* major unit amount as-is.
|
||||||
|
*
|
||||||
|
* @see https://docs.stripe.com/currencies#zero-decimal
|
||||||
|
*/
|
||||||
|
protected const ZERO_DECIMAL_CURRENCIES = [
|
||||||
|
'bif', 'clp', 'djf', 'gnf', 'jpy', 'kmf', 'krw', 'mga', 'pyg',
|
||||||
|
'rwf', 'ugx', 'vnd', 'vuv', 'xaf', 'xof', 'xpf',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Three-decimal currencies, per Stripe. The amount sent to Stripe is the
|
||||||
|
* major unit amount multiplied by 1000.
|
||||||
|
*
|
||||||
|
* @see https://docs.stripe.com/currencies#three-decimal
|
||||||
|
*/
|
||||||
|
protected const THREE_DECIMAL_CURRENCIES = ['bhd', 'jod', 'kwd', 'omr', 'tnd'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HUF, TWD and UGX are ISO zero-decimal currencies, but Stripe still
|
||||||
|
* requires amounts to be sent as if they had two decimal places.
|
||||||
|
*
|
||||||
|
* @see https://docs.stripe.com/currencies#special-cases
|
||||||
|
*/
|
||||||
|
protected const SPECIAL_ZERO_DECIMAL_CURRENCIES = ['huf', 'twd', 'ugx'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a Lunar price value to the amount expected by Stripe.
|
||||||
|
*
|
||||||
|
* Lunar stores prices as integers scaled by `Currency::decimal_places`,
|
||||||
|
* which merchants can set independently of what Stripe expects for a
|
||||||
|
* given currency. This converts back to the major unit amount first,
|
||||||
|
* then re-scales it to whatever sub-unit Stripe requires for the
|
||||||
|
* currency, so the result is correct regardless of how the merchant has
|
||||||
|
* configured `Currency::decimal_places`.
|
||||||
|
*
|
||||||
|
* @see https://docs.stripe.com/currencies
|
||||||
|
*/
|
||||||
|
public static function toStripeAmount(int $value, CurrencyContract $currency): int
|
||||||
|
{
|
||||||
|
return self::rescale($value, max($currency->decimal_places, 0), self::stripeDecimalPlaces($currency));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an amount received from Stripe back to a Lunar price value,
|
||||||
|
* scaled by `Currency::decimal_places`. Inverse of `toStripeAmount()`.
|
||||||
|
*/
|
||||||
|
public static function fromStripeAmount(int $amount, CurrencyContract $currency): int
|
||||||
|
{
|
||||||
|
return self::rescale($amount, self::stripeDecimalPlaces($currency), max($currency->decimal_places, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of decimal places Stripe expects amounts in for a currency.
|
||||||
|
*/
|
||||||
|
protected static function stripeDecimalPlaces(CurrencyContract $currency): int
|
||||||
|
{
|
||||||
|
$code = strtolower($currency->code);
|
||||||
|
|
||||||
|
// UGX is also in the zero-decimal list; the special case takes precedence.
|
||||||
|
if (in_array($code, self::SPECIAL_ZERO_DECIMAL_CURRENCIES, true)) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($code, self::ZERO_DECIMAL_CURRENCIES, true)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($code, self::THREE_DECIMAL_CURRENCIES, true)) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function rescale(int $value, int $fromDecimalPlaces, int $toDecimalPlaces): int
|
||||||
|
{
|
||||||
|
$exponent = $toDecimalPlaces - $fromDecimalPlaces;
|
||||||
|
|
||||||
|
if ($exponent >= 0) {
|
||||||
|
return $value * (10 ** $exponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
$divisor = 10 ** (-$exponent);
|
||||||
|
|
||||||
|
return intdiv(abs($value) + intdiv($divisor, 2), $divisor) * ($value < 0 ? -1 : 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Lunar\Stripe\Http\Middleware\StripeWebhookMiddleware;
|
|
||||||
use Modules\Core\Payment\Http\Controllers\StripeWebhookController;
|
use Modules\Core\Payment\Http\Controllers\StripeWebhookController;
|
||||||
|
use Modules\Core\Payment\Http\Middleware\StripeWebhookMiddleware;
|
||||||
|
|
||||||
Route::post(
|
Route::post(
|
||||||
config('payment.stripe.webhook_path', 'payments/stripe/webhook'),
|
config('payment.stripe.webhook_path', 'payments/stripe/webhook'),
|
||||||
|
|||||||
Reference in New Issue
Block a user