Skip to content
Merged
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
49 changes: 46 additions & 3 deletions src/Drivers/LunarCheckoutDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ public function complete(CheckoutSession $session, ?string $confirmationToken =
*/
$order = $cart->completedOrder ?: $cart->draftOrder ?: $cart->createOrder();

$this->stampContactEmail($session, $cart, $order);

/*
* How the customer chose to pay, for the panel, mails and any
* ERP feed (spec 0014 搂D). The session records the handle at the
Expand Down Expand Up @@ -388,7 +390,7 @@ public function storeShippingAddress(CheckoutSession $session, array $data): Car
// when the new address still offers it.
$previousOption = $cart->shippingAddress?->shipping_option;

$cart->setShippingAddress($this->toCartAddressData($data));
$cart->setShippingAddress($this->withContactEmail($session, $cart->shippingAddress, $data));

if ($previousOption !== null) {
$option = $this->shippingManifest->getOption($cart, $previousOption);
Expand All @@ -415,7 +417,7 @@ public function storeBillingAddress(CheckoutSession $session, array $data): Cart
{
$cart = $this->operableCart($session);

$cart->setBillingAddress($this->toCartAddressData($data));
$cart->setBillingAddress($this->withContactEmail($session, $cart->billingAddress, $data));

// Spec 0013 搂F: a wallet sheet started in collect mode asks for no
// shipping address, so this billing address is the only one the cart
Expand All @@ -425,7 +427,7 @@ public function storeBillingAddress(CheckoutSession $session, array $data): Cart
// no address and no collect option, and the hold is refused. The
// address is the customer's own either way.
if ($cart->shippingAddress === null && PickupPoints::fulfilment($cart) === PickupPoints::COLLECT) {
$cart->setShippingAddress($this->toCartAddressData($data));
$cart->setShippingAddress($this->withContactEmail($session, null, $data));
$cart = $this->setFulfilment->execute($cart->refresh(), PickupPoints::COLLECT);
}

Expand Down Expand Up @@ -841,6 +843,47 @@ private function assertContactKnown(CheckoutSession $session, Cart $cart): void
}
}

/**
* The address payload with the customer's email carried over. Lunar
* rewrites the address row wholesale and the address forms never send an
* email, so without this the contact step's email is wiped. An email the
* payload carries (a wallet sheet's) wins, then the row's own, then the
* session's.
*
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
private function withContactEmail(CheckoutSession $session, ?CartAddress $existing, array $data): array
{
$address = $this->toCartAddressData($data);
$email = $address['contact_email'] ?? $existing?->contact_email ?? $session->customer_email;

if (is_string($email) && trim($email) !== '') {
$address['contact_email'] = $email;
}

return $address;
}

/**
* Give every address on the placed order an email to write to. The cart's
* rows can still lack one (contact given after the last address write, or
* a signed-in customer known only through the account), and the order is
* what confirmations and the panel read.
*/
private function stampContactEmail(CheckoutSession $session, Cart $cart, Order $order): void
{
$email = $session->customer_email ?? $cart->user?->email;

if (! is_string($email) || trim($email) === '') {
return;
}

$order->addresses()
->where(fn ($query) => $query->whereNull('contact_email')->orWhere('contact_email', ''))
->update(['contact_email' => $email]);
}

/**
* Inverse of {@see toCheckoutAddress()}: the backend-neutral address
* payload (spec 0010 搂B) mapped onto Lunar's CartAddress columns. Absent
Expand Down
4 changes: 3 additions & 1 deletion tests/Feature/ExpressCollectFlowTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -192,5 +192,7 @@ function collectBasket(): Cart
expect($session->refresh()->status)->toBeInstanceOf(Completed::class)
->and($line->meta['collect'])->toBeTrue()
->and($line->meta['pickup_point']['handle'])->toBe('dartford')
->and($order->meta['fulfilment'])->toBe('collect');
->and($order->meta['fulfilment'])->toBe('collect')
->and($order->shippingAddress->contact_email)->toBe('terry@example.com')
->and($order->billingAddress->contact_email)->toBe('terry@example.com');
});
87 changes: 87 additions & 0 deletions tests/Feature/GuestContactEmailTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

use Illuminate\Foundation\Testing\RefreshDatabase;
use Lunar\Checkout\Contracts\CheckoutDriver;
use Lunar\Checkout\Contracts\PaymentMethodRegistry;
use Lunar\Checkout\PaymentMethods\Offline;
use Lunar\Core\Models\Order;
use Lunar\Tests\Checkout\TestCase;
use Lunar\Tests\Checkout\Utils\CheckoutCart;

uses(TestCase::class, RefreshDatabase::class);

/**
* A guest's email lives on the session after the contact step. Lunar rewrites
* an address row wholesale and the address forms never send an email, so the
* address steps must carry it over, or the placed order has nobody to write to.
*/
function guestCheckout(): array
{
$cart = CheckoutCart::orderable();
$cart->addresses()->update(['contact_email' => null]);
$session = CheckoutCart::session($cart->refresh());

return [$cart, $session];
}

function addressPayload(): array
{
return [
'first_name' => 'Terry',
'last_name' => 'Sparks',
'line1' => '1 Trade Counter Way',
'city' => 'London',
'postcode' => 'SE1 1AA',
'country_code' => 'GB',
];
}

it('keeps the contact email when the shipping address is saved after the contact step', function () {
[$cart, $session] = guestCheckout();

$this->post(route('lunar.checkout.contact.store', $session->uuid), ['email' => 'guest@example.com'])
->assertRedirect();
$this->post(route('lunar.checkout.shipping-address.store', $session->uuid), addressPayload())
->assertRedirect();

expect($cart->refresh()->shippingAddress->contact_email)->toBe('guest@example.com');
});

it('puts the contact email on the billing address', function () {
[$cart, $session] = guestCheckout();

$this->post(route('lunar.checkout.contact.store', $session->uuid), ['email' => 'guest@example.com']);
$this->post(route('lunar.checkout.billing-address.store', $session->uuid), addressPayload());

expect($cart->refresh()->billingAddress->contact_email)->toBe('guest@example.com');
});

it('prefers an email the address itself carries, as a wallet sheet sends', function () {
[$cart, $session] = guestCheckout();

$this->post(route('lunar.checkout.contact.store', $session->uuid), ['email' => 'guest@example.com']);
app(CheckoutDriver::class)->storeShippingAddress($session->refresh(), [
...addressPayload(),
'email' => 'site@example.com',
]);

expect($cart->refresh()->shippingAddress->contact_email)->toBe('site@example.com');
});

it('places a guest order whose addresses carry the contact email', function () {
app(PaymentMethodRegistry::class)->add(Offline::class);
[, $session] = guestCheckout();

// Only the session knows the email: no address step ran after contact.
$this->post(route('lunar.checkout.contact.store', $session->uuid), ['email' => 'guest@example.com']);

$this->postJson(route('lunar.checkout.pay', $session->uuid), [
'fingerprint' => CheckoutCart::fingerprint($session->refresh()),
'payment_method' => 'offline',
])->assertSuccessful();

$order = Order::query()->findOrFail((int) $session->refresh()->order_reference);

expect($order->shippingAddress->contact_email)->toBe('guest@example.com')
->and($order->billingAddress->contact_email)->toBe('guest@example.com');
});
Loading