Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/core/src/PaymentTypes/AbstractPayment.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@

abstract class AbstractPayment implements PaymentTypeInterface
{
/**
* Whether we should allow partial payments
*/
protected bool $allowPartialPayments = false;

/**
* The instance of the cart.
*/
Expand Down Expand Up @@ -76,6 +81,13 @@ public function setConfig(array $config): self
return $this;
}

public function allowPartialPayment(bool $condition = true): self
{
$this->allowPartialPayments = $condition;

return $this;
}

public function getPaymentChecks(TransactionContract $transaction): PaymentChecks
{
return new PaymentChecks;
Expand Down
6 changes: 3 additions & 3 deletions packages/stripe/resources/responses/payment_intent_paid.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"id": "{id}",
"object": "payment_intent",
"amount": 1099,
"amount_capturable": 0,
"amount_received": 0,
"amount": "{amount}",
"amount_capturable": "{amount}",
"amount_received": "{amount}",
"application": null,
"application_fee_amount": null,
"automatic_payment_methods": null,
Expand Down
6 changes: 5 additions & 1 deletion packages/stripe/src/Facades/Stripe.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,13 @@ protected static function getFacadeAccessor(): string
return 'lunar:stripe';
}

public static function fake(): void
public static function fake(array $data = []): MockClient
{
$mockClient = new MockClient;
$mockClient->next($data);

ApiRequestor::setHttpClient($mockClient);

return $mockClient;
}
}
21 changes: 20 additions & 1 deletion packages/stripe/src/MockClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class MockClient implements ClientInterface
{
public string $rBody = '{}';

public array $nextData = [];

public int $rcode = 200;

public array $rheaders = [];
Expand All @@ -24,6 +26,13 @@ public function __construct()
$this->url = 'https://checkout.stripe.com/pay/cs_test_'.Str::random(32);
}

public function next(array $data): self
{
$this->nextData = $data;

return $this;
}

public function request($method, $absUrl, $headers, $params, $hasFile, $apiMode = 'v1')
{
$id = array_slice(explode('/', $absUrl), -1)[0];
Expand All @@ -33,6 +42,7 @@ public function request($method, $absUrl, $headers, $params, $hasFile, $apiMode
if ($method == 'get' && str_contains($absUrl, 'charges/CH_LINK')) {
$this->rBody = $this->getResponse('charge_link', [
'status' => 'succeeded',
...$this->nextData,
]);

return [$this->rBody, $this->rcode, $this->rheaders];
Expand All @@ -51,6 +61,7 @@ public function request($method, $absUrl, $headers, $params, $hasFile, $apiMode
$this->rBody = $this->getResponse('charges', [
'status' => $status,
'failure_code' => $failureCode,
...$this->nextData,
]);

return [$this->rBody, $this->rcode, $this->rheaders];
Expand All @@ -68,6 +79,7 @@ public function request($method, $absUrl, $headers, $params, $hasFile, $apiMode
'payment_error' => null,
'failure_code' => null,
'captured' => true,
...$this->nextData,
]);

return [$this->rBody, $this->rcode, $this->rheaders];
Expand All @@ -84,6 +96,8 @@ public function request($method, $absUrl, $headers, $params, $hasFile, $apiMode
'payment_error' => null,
'failure_code' => null,
'captured' => true,
'amount' => 2000,
...$this->nextData,
]);

return [$this->rBody, $this->rcode, $this->rheaders];
Expand All @@ -98,13 +112,16 @@ public function request($method, $absUrl, $headers, $params, $hasFile, $apiMode
'payment_error' => 'foo',
'failure_code' => 1234,
'captured' => false,
...$this->nextData,
]);

return [$this->rBody, $this->rcode, $this->rheaders];
}

if (str_contains($absUrl, 'PI_REQUIRES_PAYMENT_METHOD')) {
$this->rBody = $this->getResponse('payment_intent_requires_payment_method');
$this->rBody = $this->getResponse('payment_intent_requires_payment_method', [
...$this->nextData,
]);

return [$this->rBody, $this->rcode, $this->rheaders];
}
Expand All @@ -118,6 +135,7 @@ public function request($method, $absUrl, $headers, $params, $hasFile, $apiMode
'payment_error' => 'foo',
'failure_code' => 1234,
'captured' => false,
...$this->nextData,
]);

return [$this->rBody, $this->rcode, $this->rheaders];
Expand All @@ -133,6 +151,7 @@ public function request($method, $absUrl, $headers, $params, $hasFile, $apiMode
'payment_error' => $succeeded ? null : 'failed',
'failure_code' => $succeeded ? null : 1234,
'captured' => $succeeded,
...$this->nextData,
]);

$this->failThenCaptureCalled = true;
Expand Down
13 changes: 13 additions & 0 deletions packages/stripe/src/StripePaymentType.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,19 @@ final public function authorize(): ?PaymentAuthorize
$paymentIntentId
);

if (! $this->allowPartialPayments && $this->order->total->value != $this->paymentIntent->amount) {
$failure = new PaymentAuthorize(
success: false,
message: 'Captured amount mismatch',
orderId: $this->order->id,
paymentType: 'stripe',
);

PaymentAttemptEvent::dispatch($failure);

return $failure;
}

if (! $this->paymentIntent) {
$failure = new PaymentAuthorize(
success: false,
Expand Down
80 changes: 73 additions & 7 deletions tests/stripe/Unit/StripePaymentTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@

use function Pest\Laravel\assertDatabaseHas;

uses(\Lunar\Tests\Stripe\Unit\TestCase::class);
uses(\Lunar\Tests\Stripe\Unit\TestCase::class)->group('stripe');

it('can capture an order', function () {
$cart = CartBuilder::build();
$payment = new StripePaymentType;

Stripe::fake()->next([
'amount' => $cart->calculate()->total->value,
]);

$response = $payment->cart($cart)->withData([
'payment_intent' => 'PI_CAPTURE',
])->authorize();
Expand All @@ -27,11 +31,63 @@
'order_id' => $cart->refresh()->completedOrder->id,
'type' => 'capture',
]);
})->group('this');
});

it('wont capture an order with lesser intent amount', function () {
$cart = CartBuilder::build();
$payment = new StripePaymentType;

Stripe::fake()->next([
'amount' => 100,
]);

$response = $payment->cart($cart)->withData([
'payment_intent' => 'PI_CAPTURE',
])->authorize();

expect($response)->toBeInstanceOf(PaymentAuthorize::class)
->and($response->success)->toBeFalse()
->and($cart->refresh()->completedOrder)->toBeNull()
->and($cart->refresh()->draftOrder)->not()->toBeNull()
->and($cart->paymentIntents->first()->intent_id)->toEqual('PI_CAPTURE');

\Pest\Laravel\assertDatabaseMissing((new Transaction)->getTable(), [
'order_id' => $cart->refresh()->draftOrder->id,
'type' => 'capture',
]);
});

it('will capture an order with lesser intent amount if allowed', function () {
$cart = CartBuilder::build();
$payment = new StripePaymentType;

Stripe::fake()->next([
'amount' => 100,
]);

$response = $payment->cart($cart)->withData([
'payment_intent' => 'PI_CAPTURE',
])->allowPartialPayment()->authorize();

expect($response)->toBeInstanceOf(PaymentAuthorize::class)
->and($response->success)->toBeTrue()
->and($cart->refresh()->completedOrder)->not()->toBeNull()
->and($cart->refresh()->draftOrder)->toBeNull()
->and($cart->paymentIntents->first()->intent_id)->toEqual('PI_CAPTURE');

\Pest\Laravel\assertDatabaseHas((new Transaction)->getTable(), [
'order_id' => $cart->refresh()->completedOrder->id,
'type' => 'capture',
]);
});

it('can handle failed payments', function () {
$cart = CartBuilder::build();

Stripe::fake()->next([
'amount' => $cart->calculate()->total->value,
]);

$payment = new StripePaymentType;

$response = $payment->cart($cart)->withData([
Expand All @@ -48,7 +104,7 @@
'type' => 'capture',
'success' => false,
]);
})->group('noo');
});

it('can retrieve existing payment intent', function () {
$cart = CartBuilder::build([
Expand All @@ -57,15 +113,20 @@
],
]);

Stripe::createIntent($cart->calculate());
Stripe::createIntent($cart->calculate(), []);

expect($cart->refresh()->meta['payment_intent'])->toBe('PI_FOOBAR');
});

it('can handle multiple payment events', function () {

$cart = CartBuilder::build();
$order = $cart->createOrder();

Stripe::fake()->next([
'amount' => $order->total->value,
]);

$payment = new StripePaymentType;

$response = $payment->order($order)->withData([
Expand All @@ -78,9 +139,6 @@
->and($cart->currentDraftOrder())->not()->toBeNull()
->and($cart->paymentIntents->first()->intent_id)->toEqual('PI_FIRST_FAIL_THEN_CAPTURE');

// $cart->refresh();
// $cart->paymentIntents->first()->refresh();

$response = $payment->order($order)->withData([
'payment_intent' => 'PI_FIRST_FAIL_THEN_CAPTURE',
])->authorize();
Expand All @@ -96,6 +154,10 @@
$cart = CartBuilder::build();
$order = $cart->createOrder();

Stripe::fake()->next([
'amount' => $cart->calculate()->total->value,
]);

$payment = new StripePaymentType;

$response = $payment->order($order)->withData([
Expand Down Expand Up @@ -126,6 +188,10 @@
'placed_at' => now(),
]);

Stripe::fake()->next([
'amount' => $cart->calculate()->total->value,
]);

$payment = new StripePaymentType;

$response = $payment->cart($cart)->withData([
Expand Down