Skip to content

Commit 09ebe0e

Browse files
Pay on delivery (#90)
1 parent 4e94a27 commit 09ebe0e

File tree

15 files changed

+254
-3
lines changed

15 files changed

+254
-3
lines changed

docs/docs/payment-gateways.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,65 @@ You can workaround this by setting up a tunneling service, like [Expose](https:/
336336

337337
You will need to update the `APP_URL` key in your `.env` while your tunnel is active, so the gateway points towards the tunnel.
338338

339+
## Pay on delivery
340+
In some markets, you may wish to offer "Pay on delivery" instead of requiring payment upfront. To do this, simply add the built-in `pay_on_delivery` payment gateway to your gateways array:
341+
342+
```php
343+
// config/statamic/cargo.php
344+
345+
'gateways' => [
346+
'pay_on_delivery' => [], // [tl! add]
347+
],
348+
```
349+
350+
:::tip note
351+
The "Pay on delivery" gateway will only be shown to customers when their selected shipping option supports it.
352+
353+
```php
354+
// app/ShippingMethods/LocalPostageService.php
355+
356+
public function options(Cart $cart): Collection
357+
{
358+
return collect([
359+
ShippingOption::make($this)
360+
->name(__('Standard Shipping'))
361+
->price(499)
362+
->acceptsPaymentOnDelivery(true), // [tl! add]
363+
]);
364+
}
365+
```
366+
:::
367+
368+
When orders are placed using "Pay on delivery":
369+
- They'll start with a status of "Payment Pending"
370+
- When you ship the order, update the status to "Shipped"
371+
- Once the delivery company has collected payment and confirmed delivery, update the status to "Payment Received"
372+
373+
Depending on the delivery company, you may be able to automate the final status update via a custom API integration.
374+
375+
### Payment Form
376+
377+
:::tip note
378+
You don't need to copy this into your project if you're using the [built-in checkout flow](/frontend/checkout/prebuilt), as you'll already have it.
379+
:::
380+
381+
To use the Pay on delivery gateway, copy and paste this template into your checkout flow:
382+
383+
::tabs
384+
::tab antlers
385+
```antlers
386+
<form x-data="{ busy: false }" action="{{ checkout_url }}" method="POST" @submit="busy = true">
387+
<button>Place Order</button>
388+
</form>
389+
```
390+
::tab blade
391+
```blade
392+
<form x-data="{ busy: false }" action="{{ $checkout_url }}" method="POST" @submit="busy = true">
393+
<button>Place Order</button>
394+
</form>
395+
```
396+
::
397+
339398
## Build your own
340399
If you need to use a payment processor that Cargo doesn't support out-of-the-box, it's pretty easy to build your own payment gateway.
341400

docs/docs/shipping.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ class RoyalMail extends ShippingMethod
4040

4141
The `options` method should return a collection of `ShippingOption` objects. The name and the price are displayed to the customer during checkout.
4242

43+
If you want to accept [payment on delivery](/docs/payment-gateways#pay-on-delivery), you'll also need to chain `->acceptsPaymentOnDelivery(true)` when creating shipping options.
44+
4345
You can optionally provide a `fieldtypeDetails` method to your shipping method, allowing you to display information about the shipment in the Control Panel, under the "Shipping" tab:
4446

4547
```php
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<form x-data="{ busy: false }" action="{{ checkout_url }}" method="POST" @submit="busy = true">
2+
<div>
3+
{{ partial:checkout/components/button label="Place Order" type="submit" }}
4+
</div>
5+
</form>

src/Cart/Cart.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ public function shippingOption(): ?ShippingOption
128128
return ShippingOption::make($this->shippingMethod())
129129
->name(Arr::get($this->get('shipping_option'), 'name'))
130130
->handle(Arr::get($this->get('shipping_option'), 'handle'))
131-
->price(Arr::get($this->get('shipping_option'), 'price'));
131+
->price(Arr::get($this->get('shipping_option'), 'price'))
132+
->acceptsPaymentOnDelivery(Arr::get($this->get('shipping_option'), 'accepts_payment_on_delivery'));
132133
}
133134

134135
public function paymentGateway(): ?PaymentGateway

src/Http/Controllers/CartPaymentGatewaysController.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public function __invoke()
1717
$cart = CartFacade::current();
1818

1919
return Facades\PaymentGateway::all()
20+
->filter(fn (PaymentGateway $paymentGateway) => $paymentGateway->isAvailable($cart))
2021
->map(function (PaymentGateway $paymentGateway) use ($cart) {
2122
$setup = $cart->isFree() ? [] : $paymentGateway->setup($cart);
2223

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
namespace DuncanMcClean\Cargo\Payments\Gateways;
4+
5+
use DuncanMcClean\Cargo\Cargo;
6+
use DuncanMcClean\Cargo\Contracts\Cart\Cart;
7+
use DuncanMcClean\Cargo\Contracts\Orders\Order;
8+
use DuncanMcClean\Cargo\Orders\TimelineEvent;
9+
use DuncanMcClean\Cargo\Orders\TimelineEventTypes\OrderStatusChanged;
10+
use DuncanMcClean\Cargo\Support\Money;
11+
use Illuminate\Http\Request;
12+
use Illuminate\Http\Response;
13+
14+
class PayOnDelivery extends PaymentGateway
15+
{
16+
protected static $title = 'Pay on delivery';
17+
18+
public function isAvailable(Cart $cart): bool
19+
{
20+
return $cart->shippingOption()?->acceptsPaymentOnDelivery() ?? false;
21+
}
22+
23+
public function setup(Cart $cart): array
24+
{
25+
return [];
26+
}
27+
28+
public function process(Order $order): void
29+
{
30+
//
31+
}
32+
33+
public function capture(Order $order): void
34+
{
35+
//
36+
}
37+
38+
public function cancel(Cart $cart): void
39+
{
40+
//
41+
}
42+
43+
public function webhook(Request $request): Response
44+
{
45+
return response();
46+
}
47+
48+
public function refund(Order $order, int $amount): void
49+
{
50+
$order->set('amount_refunded', $amount)->save();
51+
}
52+
53+
public function logo(): ?string
54+
{
55+
return Cargo::svg('cargo-mark');
56+
}
57+
58+
public function fieldtypeDetails(Order $order): array
59+
{
60+
$paymentReceivedEvent = $order->timelineEvents()
61+
->filter(function (TimelineEvent $timelineEvent): bool {
62+
return get_class($timelineEvent->type()) === OrderStatusChanged::class
63+
&& $timelineEvent->metadata()->get('New Status') === 'payment_received';
64+
})
65+
->first();
66+
67+
return [
68+
__('Amount') => Money::format($order->grandTotal(), $order->site()),
69+
__('Payment Date') => $paymentReceivedEvent
70+
? __(':datetime UTC', ['datetime' => $paymentReceivedEvent->datetime()->format('Y-m-d H:i:s')])
71+
: __('Awaiting Payment'),
72+
];
73+
}
74+
}

src/Payments/Gateways/PaymentGateway.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ abstract class PaymentGateway
1616
{
1717
use HasHandle, HasTitle, RegistersItself;
1818

19+
public function isAvailable(Cart $cart): bool
20+
{
21+
return true;
22+
}
23+
1924
abstract public function setup(Cart $cart): array;
2025

2126
abstract public function process(Order $order): void;

src/Payments/PaymentServiceProvider.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class PaymentServiceProvider extends AddonServiceProvider
1010
protected array $paymentGateways = [
1111
Gateways\Dummy::class,
1212
Gateways\Mollie::class,
13+
Gateways\PayOnDelivery::class,
1314
Gateways\Stripe::class,
1415
];
1516

src/Shipping/FreeShipping.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ public function options(Cart $cart): Collection
1515
return collect([
1616
ShippingOption::make($this)
1717
->name(__('Free Shipping'))
18-
->price(0),
18+
->price(0)
19+
->acceptsPaymentOnDelivery(true),
1920
]);
2021
}
2122

src/Shipping/ShippingOption.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class ShippingOption implements Augmentable, Purchasable
2121
public $name;
2222
public $handle;
2323
public $price;
24+
public $acceptsPaymentOnDelivery = false;
2425
public $shippingMethod;
2526

2627
public static function make(ShippingMethod $shippingMethod): self
@@ -51,6 +52,11 @@ public function price($price = null)
5152
return $this->fluentlyGetOrSet('price')->args(func_get_args());
5253
}
5354

55+
public function acceptsPaymentOnDelivery($acceptsPaymentOnDelivery = null)
56+
{
57+
return $this->fluentlyGetOrSet('acceptsPaymentOnDelivery')->args(func_get_args());
58+
}
59+
5460
public function shippingMethod($shippingMethod = null)
5561
{
5662
return $this->fluentlyGetOrSet('shippingMethod')
@@ -105,6 +111,7 @@ public function augmentedArrayData(): array
105111
'name' => $this->name(),
106112
'handle' => $this->handle(),
107113
'price' => $this->price(),
114+
'accepts_payment_on_delivery' => $this->acceptsPaymentOnDelivery(),
108115
'shipping_method' => $this->shippingMethod(),
109116
];
110117
}
@@ -115,6 +122,7 @@ public function toArray(): array
115122
'name' => $this->name(),
116123
'handle' => $this->handle(),
117124
'price' => $this->price(),
125+
'accepts_payment_on_delivery' => $this->acceptsPaymentOnDelivery(),
118126
];
119127
}
120128
}

0 commit comments

Comments
 (0)