Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
duncanmcclean committed Jan 14, 2025
1 parent b6e7851 commit 1f2e579
Show file tree
Hide file tree
Showing 29 changed files with 500 additions and 762 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
},
"laravel": {
"providers": [
"DuncanMcClean\\SimpleCommerce\\ServiceProvider"
"DuncanMcClean\\SimpleCommerce\\ServiceProvider",
"DuncanMcClean\\SimpleCommerce\\Payments\\PaymentServiceProvider"
]
}
},
Expand Down
15 changes: 13 additions & 2 deletions config/simple-commerce.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,19 @@

'payments' => [
'gateways' => [
\DuncanMcClean\SimpleCommerce\Payments\Gateways\DummyGateway::class => [
'display' => 'Card',
'dummy' => [
//
],

'stripe' => [
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_SECRET'),
'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
],

'mollie' => [
'key' => env('MOLLIE_KEY'),
'profile_id' => env('MOLLIE_PROFILE_ID'),
],
],
],
Expand Down
7 changes: 4 additions & 3 deletions routes/actions.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use DuncanMcClean\SimpleCommerce\Http\Controllers\CartController;
use DuncanMcClean\SimpleCommerce\Http\Controllers\CartLineItemsController;
use DuncanMcClean\SimpleCommerce\Http\Controllers\CartPaymentGatewaysController;
use DuncanMcClean\SimpleCommerce\Http\Controllers\CartShippingController;
use DuncanMcClean\SimpleCommerce\Http\Controllers\CheckoutController;
use DuncanMcClean\SimpleCommerce\Http\Controllers\DigitalProducts\DownloadController;
Expand All @@ -28,10 +29,10 @@

Route::name('payments.')
->prefix('payments')
->withoutMiddleware(VerifyCsrfToken::class)
->withoutMiddleware(['App\Http\Middleware\VerifyCsrfToken', 'Illuminate\Foundation\Http\Middleware\VerifyCsrfToken'])
->group(function () {
Route::get('{gateway}/callback', CallbackController::class)->name('callback');
Route::post('{gateway}/webhook', WebhookController::class)->name('webhook');
Route::get('{paymentGateway}/callback', CallbackController::class)->name('callback');
Route::post('{paymentGateway}/webhook', WebhookController::class)->name('webhook');
});

Route::name('digital-products')
Expand Down
3 changes: 3 additions & 0 deletions src/Cart/Cart.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use DuncanMcClean\SimpleCommerce\Contracts\Shipping\ShippingMethod as ShippingMethodContract;
use DuncanMcClean\SimpleCommerce\Customers\GuestCustomer;
use DuncanMcClean\SimpleCommerce\Data\HasAddresses;
use DuncanMcClean\SimpleCommerce\Events\CartRecalculated;
use DuncanMcClean\SimpleCommerce\Events\CartSaved;
use DuncanMcClean\SimpleCommerce\Facades\Cart as CartFacade;
use DuncanMcClean\SimpleCommerce\Facades\Coupon as CouponFacade;
Expand Down Expand Up @@ -248,6 +249,8 @@ public function recalculate(): void
app(Calculator::class)->calculate($this);

$this->set('fingerprint', $this->fingerprint());

event(new CartRecalculated($this));
}

public function fingerprint(): string
Expand Down
82 changes: 1 addition & 81 deletions src/Contracts/Payments/Gateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,87 +2,7 @@

namespace DuncanMcClean\SimpleCommerce\Contracts\Payments;

use DuncanMcClean\SimpleCommerce\Contracts\Orders\Order;
use Illuminate\Http\Request;

interface Gateway
{
/**
* This method should return the name of the payment gateway. This name can be
* overridden by sites in their config.
*/
public function name(): string;

/**
* If your payment gateway is off-site (eg. your customer doesn't have to submit the
* {{ checkout }} form to confirm the payment), then you should return true here.
*/
public function isOffsiteGateway(): bool;

/**
* This method is called when the {{ checkout }} tag is used. It should return any
* data you need in the front-end to handle a payment (like a Stripe Payment Intent).
*
* If you're building an off-site gateway, you should return a `checkout_url` key with the
* URL the user should be redirected to for checkout.
*/
public function prepare(Request $request, Order $order): array;

/**
* This method is called when you submit the {{ checkout }} form. It should return
* an array of payment data that'll be saved onto the order.
*
* If you need to display an error message, you should throw a GatewayCheckoutFailed exception.
*
* If you're building an off-site gateway, you don't need to implement this method.
*/
public function checkout(Request $request, Order $order): array;

/**
* This method should return an array of validation rules that'll be run whenever
* the {{ checkout }} has been submitted.
*
* If you're building an off-site gateway, you don't need to implement this method.
*/
public function checkoutRules(): array;

/**
* This method should return an array of validation messages that'll be used whenever
* the {{ checkout }} has been submitted. This method isn't mandatory.
*
* If you're building an off-site gateway, you don't need to implement this method.
*/
public function checkoutMessages(): array;

/**
* When given an order, this method should process the refund of an order. You should
* return an array of any data which may prove helpful in the future to track down
* refunds (like a Refund ID).
*
* @return array|null
*/
public function refund(Order $order): array;

/**
* This method will be called when users are redirected back to your site after
* an off-site checkout. You should return true if the payment was successful.
*/
public function callback(Request $request): bool;

/**
* This method will be called when a webhook is received from the payment gateway.
* This is where you should handle any updates to order statuses.
*
* Whatever you return from this method will be sent back as the webhook's response.
*/
public function webhook(Request $request);

/**
* This method should return an array containing `text` and `url` keys. The `text` key
* should return something unique to the payment & the `url` should return a URL to
* the payment in the payment gateway's dashboard.
*
* @param mixed $value
*/
public function fieldtypeDisplay($value): array;
// todo
}
3 changes: 3 additions & 0 deletions src/Data/Address.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace DuncanMcClean\SimpleCommerce\Data;

use Statamic\Dictionaries\Item;
use Statamic\Facades\Dictionary;

class Address
{
public function __construct(
Expand Down
16 changes: 16 additions & 0 deletions src/Events/CartRecalculated.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace DuncanMcClean\SimpleCommerce\Events;

use DuncanMcClean\SimpleCommerce\Contracts\Cart\Cart;
use Statamic\Contracts\Git\ProvidesCommitMessage;

class CartRecalculated implements ProvidesCommitMessage
{
public function __construct(public Cart $cart) {}

public function commitMessage()
{
return __('Cart recalculated', [], config('statamic.git.locale'));
}
}
11 changes: 11 additions & 0 deletions src/Exceptions/PaymentGatewayDoesNotExist.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace DuncanMcClean\SimpleCommerce\Exceptions;

class PaymentGatewayDoesNotExist extends \Exception
{
public function __construct(string $paymentGateway)
{
parent::__construct("Payment gateway [{$paymentGateway}] does not exist.");
}
}
2 changes: 1 addition & 1 deletion src/Exceptions/ShippingMethodDoesNotExist.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ class ShippingMethodDoesNotExist extends \Exception
{
public function __construct(string $shippingMethod)
{
parent::__construct("ShippingMethod method [{$shippingMethod}] does not exist.");
parent::__construct("Shipping method [{$shippingMethod}] does not exist.");
}
}
19 changes: 0 additions & 19 deletions src/Facades/Gateway.php

This file was deleted.

17 changes: 17 additions & 0 deletions src/Facades/PaymentGateway.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace DuncanMcClean\SimpleCommerce\Facades;

use DuncanMcClean\SimpleCommerce\Payments\Gateways\Manager;
use Illuminate\Support\Facades\Facade;

/**
* @see \DuncanMcClean\SimpleCommerce\Payments\Gateways\Manager
*/
class PaymentGateway extends Facade
{
protected static function getFacadeAccessor()
{
return Manager::class;
}
}
4 changes: 4 additions & 0 deletions src/Http/Controllers/CheckoutController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use DuncanMcClean\SimpleCommerce\Events\CouponRedeemed;
use DuncanMcClean\SimpleCommerce\Facades\Cart;
use DuncanMcClean\SimpleCommerce\Facades\Order;
use DuncanMcClean\SimpleCommerce\Facades\PaymentGateway;
use DuncanMcClean\SimpleCommerce\Http\Resources\API\CartResource;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
Expand Down Expand Up @@ -46,6 +47,9 @@ public function __invoke(Request $request)
$order = Order::makeFromCart($cart);
$order->save();

$gateway = PaymentGateway::find($cart->get('payment_gateway'));
$gateway->process($order, $request);

if ($order->coupon()) {
event(new CouponRedeemed($order->coupon(), $order));
}
Expand Down
67 changes: 36 additions & 31 deletions src/Http/Controllers/Payments/CallbackController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,58 @@

namespace DuncanMcClean\SimpleCommerce\Http\Controllers\Payments;

use DuncanMcClean\SimpleCommerce\Exceptions\GatewayCallbackMethodDoesNotExist;
use DuncanMcClean\SimpleCommerce\Exceptions\GatewayDoesNotExist;
use DuncanMcClean\SimpleCommerce\Facades\Gateway;
use DuncanMcClean\SimpleCommerce\Events\CouponRedeemed;
use DuncanMcClean\SimpleCommerce\Facades\Cart;
use DuncanMcClean\SimpleCommerce\Facades\Order;
use DuncanMcClean\SimpleCommerce\Orders\OrderStatus;
use DuncanMcClean\SimpleCommerce\Orders\PaymentStatus;
use DuncanMcClean\SimpleCommerce\SimpleCommerce;
use DuncanMcClean\SimpleCommerce\Facades\PaymentGateway;
use Illuminate\Http\Request;
use Statamic\Exceptions\NotFoundHttpException;

class CallbackController
{
public function __invoke(Request $request, $gateway)
public function __invoke(Request $request, string $paymentGateway)
{
if ($request->has('_order_id')) {
$order = Order::find($request->get('_order_id'));
} else {
$order = $this->getCart();
}
$cart = Cart::current();
$paymentGateway = PaymentGateway::find($paymentGateway);

$gatewayName = $gateway;
throw_if(! $paymentGateway, NotFoundHttpException::class);

$gateway = SimpleCommerce::gateways()
->where('handle', $gateway)
->first();
if (! $cart->customer()) {
$paymentGateway->cancel($cart);

if (! $gateway) {
throw new GatewayDoesNotExist("Gateway [{$gatewayName}] does not exist.");
// todo: url should be customizable
return redirect('/checkout')->withErrors([
'checkout' => __('Order cannot be created without customer information.'),
]);
}

try {
$callbackSuccess = Gateway::use($gateway['handle'])->callback($request);
} catch (GatewayCallbackMethodDoesNotExist $e) {
$callbackSuccess = $order->paymentStatus() === PaymentStatus::Paid;
if (! $cart->taxableAddress()) {
$paymentGateway->cancel($cart);

// todo: url should be customizable
return redirect('/checkout')->withErrors([
'checkout' => __('Order cannot be created without an address.'),
]);
}

if (! $callbackSuccess) {
return $this->withErrors($request, "Order [{$order->get('title')}] has not been marked as paid yet.");
$order = Order::query()->where('cart', $cart->id())->first();

if (! $order) {
$order = Order::makeFromCart($cart);
$order->save();

if ($order->coupon()) {
event(new CouponRedeemed($order->coupon(), $order)); // todo: consider whether this is the right timing for this event
}
}

$order->status(OrderStatus::Placed)->save();
$paymentGateway->process($order, $request);

$this->forgetCart();
// todo: uncomment this when we figure out how to load the *old* cart on the confirmation
// page AND get rid of the cart when we're done with it
// Cart::forgetCurrentCart();

return $this->formSuccess($request, [
'success' => __('Checkout Complete!'),
'cart' => $order->toAugmentedArray(),
'is_checkout_request' => true,
]);
// todo: make this configurable (how... i don't know)
return redirect("/checkout/complete?order_id={$order->id()}");
}
}
22 changes: 7 additions & 15 deletions src/Http/Controllers/Payments/WebhookController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,20 @@

namespace DuncanMcClean\SimpleCommerce\Http\Controllers\Payments;

use DuncanMcClean\SimpleCommerce\Events\GatewayWebhookReceived;
use DuncanMcClean\SimpleCommerce\Exceptions\GatewayDoesNotExist;
use DuncanMcClean\SimpleCommerce\Facades\Gateway;
use DuncanMcClean\SimpleCommerce\SimpleCommerce;
use DuncanMcClean\SimpleCommerce\Facades\PaymentGateway;
use Illuminate\Http\Request;
use Statamic\Exceptions\NotFoundHttpException;

class WebhookController
{
public function __invoke(Request $request, $gateway)
public function __invoke(Request $request, string $paymentGateway)
{
$gatewayName = $gateway;
$paymentGateway = PaymentGateway::find($paymentGateway);

$gateway = SimpleCommerce::gateways()
->where('handle', $gateway)
->first();
throw_if(! $paymentGateway, NotFoundHttpException::class);

if (! $gateway) {
throw new GatewayDoesNotExist("Gateway [{$gatewayName}] does not exist.");
}
$paymentGateway->webhook($request);

event(new GatewayWebhookReceived($request->all()));

return Gateway::use($gateway['handle'])->webhook($request);
return 'Webhook handled';
}
}
Loading

0 comments on commit 1f2e579

Please sign in to comment.