Feature: Moving Payment Methods to DB, adding fees, Transaction Updates, Refund Updates, General Updates to Payments
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Payment\Services;
|
||||
|
||||
/**
|
||||
* Which payment driver CLASSES exist this deploy — the code-registry layer,
|
||||
* mirroring Lunar\Shipping\Managers\ShippingManager's own built-in-methods
|
||||
* + Manager::extend() pattern, but purpose-built rather than extending
|
||||
* Illuminate\Support\Manager: Manager's create{X}Driver() convention fits
|
||||
* a uniform one-interface-per-driver contract (ShippingRateInterface); a
|
||||
* Payment driver instead implements several independent, opt-in capability
|
||||
* interfaces at once (Configurable, SupportsPay, SupportsAuthorization,
|
||||
* ...), so there's no single "the" method to generate per driver.
|
||||
*
|
||||
* Deliberately knows NOTHING about Modules\Core\Payment\Models\PaymentMethod
|
||||
* or the database — resolve() is a pure "does this key still exist"
|
||||
* lookup. Whether a resolved driver is administratively enabled, or
|
||||
* reports itself Configurable::isConfigured(), is the DOMAIN's job
|
||||
* (Modules\Core\Checkout\Services\CheckoutService::getPaymentMethods()) —
|
||||
* see docs/payments.md. This split is what lets the identical registry
|
||||
* shape be lifted for a future Invoicing/AntiFraud domain without dragging
|
||||
* Payment-specific concepts along with it.
|
||||
*
|
||||
* Built-in drivers are registered in Modules\Core\Providers\
|
||||
* PaymentServiceProvider::boot() via register(); a consuming app or a
|
||||
* future payment-provider package registers its own the same way, from
|
||||
* its own service provider's boot() — exactly how Shipping::extend() works
|
||||
* for ACS/Box Now (src/Providers/ShippingServiceProvider.php).
|
||||
*/
|
||||
class PaymentDriverRegistry
|
||||
{
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private array $drivers = [];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private array $labels = [];
|
||||
|
||||
/**
|
||||
* $key is the registry key a Modules\Core\Payment\Models\PaymentMethod
|
||||
* row's own `driver` column stores — NOT the same as that row's `type`
|
||||
* (its merchant-facing slug). Two rows can share one driver key (e.g.
|
||||
* both 'cash-on-delivery' and 'cash-in-hand' using the same 'offline'
|
||||
* driver with different type/name/fee).
|
||||
*
|
||||
* $label is a short, human-readable name (e.g. "Stripe", "Offline /
|
||||
* Manual") — this is where that comes from, not $driverClass's own
|
||||
* FQCN. Payment's driver classes implement several independent,
|
||||
* opt-in capability interfaces (Configurable, SupportsPay, ...), none
|
||||
* of which carries a display name the way Lunar\Shipping\Interfaces\
|
||||
* ShippingRateInterface::name() does for every shipping driver — the
|
||||
* registry is the one place that DOES know every driver at once, so
|
||||
* it's the natural (and only) place to also hold this.
|
||||
*/
|
||||
public function register(string $key, string $driverClass, string $label): void
|
||||
{
|
||||
$this->drivers[$key] = $driverClass;
|
||||
$this->labels[$key] = $label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Null if $key was never registered — deliberately non-throwing, same
|
||||
* reasoning the old PaymentDriverResolver already had: a caller
|
||||
* checking availability (or the payment:sync-drivers command checking
|
||||
* every PaymentMethod row) needs "not found" to be a normal, silent
|
||||
* result, not an exception to catch.
|
||||
*/
|
||||
public function resolve(string $key): ?object
|
||||
{
|
||||
$driverClass = $this->drivers[$key] ?? null;
|
||||
|
||||
return $driverClass ? app($driverClass) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every registered key => driver class — what payment:sync-drivers
|
||||
* checks every PaymentMethod row's `driver` column against.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return $this->drivers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every registered key => human-readable label — what a Filament
|
||||
* driver Select populates its options from (mirroring
|
||||
* ShippingMethodResourceExtension::driverSelect()'s use of
|
||||
* Shipping::getSupportedDrivers(), which reads each driver's own
|
||||
* name()) — never the raw class name from all().
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function labels(): array
|
||||
{
|
||||
return $this->labels;
|
||||
}
|
||||
|
||||
public function label(string $key): ?string
|
||||
{
|
||||
return $this->labels[$key] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Payment\Services;
|
||||
|
||||
/**
|
||||
* Resolves a payment type key (e.g. 'stripe', 'cash-on-delivery') to its
|
||||
* registered driver instance — extracted out of CheckoutService so both it
|
||||
* and anything else needing the same lookup share one implementation
|
||||
* instead of duplicating this config read.
|
||||
*
|
||||
* Returns a plain object, not a shared interface — Payment's own drivers
|
||||
* implement several independent, orthogonal capability interfaces at once
|
||||
* (Configurable, SupportsPay, SupportsAuthorization, ...; see
|
||||
* StripePaymentDriver implementing all six). There is no single common
|
||||
* "PaymentDriver" contract to type this against; a caller checks
|
||||
* `instanceof SupportsPay` / `instanceof SupportsAuthorization` itself,
|
||||
* the same way Payment's own contracts are designed to be consumed.
|
||||
*/
|
||||
class PaymentDriverResolver
|
||||
{
|
||||
/**
|
||||
* Null if $type has no 'payment_driver' registered in
|
||||
* config('lunar.payments.types.<type>') at all — deliberately
|
||||
* non-throwing so a caller like CheckoutService::getPaymentMethods()
|
||||
* can filter unresolvable types silently rather than treating "not
|
||||
* registered" as an error condition when just checking availability.
|
||||
*/
|
||||
public function resolve(string $type): ?object
|
||||
{
|
||||
$driverClass = config("lunar.payments.types.{$type}.payment_driver");
|
||||
|
||||
return $driverClass ? app($driverClass) : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Payment\Services;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Modules\Core\Payment\Models\PaymentMethod;
|
||||
|
||||
/**
|
||||
* Cached read layer over PaymentMethod — the single source both
|
||||
* Modules\Core\Checkout\Services\CheckoutService (checkout-time
|
||||
* availability) and anything else needing the payment-method list (e.g.
|
||||
* PaymentServiceProvider's Lunar\Facades\Payments shim, order screens
|
||||
* showing a transaction's driver) read from, so the table is fetched once
|
||||
* per cache lifetime rather than once per caller/request. Mirrors
|
||||
* Modules\Core\Localization\Services\LanguageCache's exact shape.
|
||||
*
|
||||
* Cached forever, invalidated via forget() by
|
||||
* Modules\Core\Payment\Observers\FlushPaymentMethodCache on
|
||||
* PaymentMethod::saved()/deleted() — no bespoke Created/Updated/Deleted
|
||||
* event trio needed, unlike LanguageCache's (Language is a Lunar-owned
|
||||
* model reacted to indirectly); PaymentMethod is entirely our own model,
|
||||
* so a plain Eloquent observer is the direct route.
|
||||
*/
|
||||
class PaymentMethodCache
|
||||
{
|
||||
private const CACHE_KEY = 'core.payment.methods';
|
||||
|
||||
public function all(): Collection
|
||||
{
|
||||
return Cache::rememberForever(
|
||||
self::CACHE_KEY,
|
||||
fn () => PaymentMethod::query()->orderBy('position')->get(),
|
||||
);
|
||||
}
|
||||
|
||||
public function forget(): void
|
||||
{
|
||||
Cache::forget(self::CACHE_KEY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Payment\Services;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Modules\Core\Payment\Events\PaymentMethodCreated;
|
||||
use Modules\Core\Payment\Events\PaymentMethodDeleted;
|
||||
use Modules\Core\Payment\Events\PaymentMethodUpdated;
|
||||
use Modules\Core\Payment\Models\PaymentMethod;
|
||||
|
||||
/**
|
||||
* The single write (AND read) gateway for PaymentMethod — every Filament
|
||||
* resource/action calls this, not PaymentMethod::create()/update()/delete()
|
||||
* directly, so cache invalidation is one explicit step colocated with the
|
||||
* mutation (not hidden in a model observer) and every admin change to a
|
||||
* payment method dispatches a matching event, the same convention
|
||||
* Modules\Core\Cart\Services\CartService already established for its own
|
||||
* mutating methods.
|
||||
*
|
||||
* list() is what PaymentMethodCache actually reads through — see that
|
||||
* class for why this needs caching at all (Modules\Core\Checkout\
|
||||
* Services\CheckoutService and PaymentServiceProvider's Lunar\Facades\
|
||||
* Payments shim both read the full payment-method list on the hot path).
|
||||
*/
|
||||
class PaymentMethodService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PaymentMethodCache $cache,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return Collection<int, PaymentMethod>
|
||||
*/
|
||||
public function list(): Collection
|
||||
{
|
||||
return $this->cache->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function create(array $data): PaymentMethod
|
||||
{
|
||||
$method = PaymentMethod::create($data);
|
||||
|
||||
$this->cache->forget();
|
||||
|
||||
Event::dispatch(new PaymentMethodCreated($method));
|
||||
|
||||
return $method;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function update(PaymentMethod $method, array $data): PaymentMethod
|
||||
{
|
||||
$old = $method->only(array_keys($data));
|
||||
|
||||
$method->update($data);
|
||||
|
||||
$this->cache->forget();
|
||||
|
||||
Event::dispatch(new PaymentMethodUpdated($method, $old));
|
||||
|
||||
return $method;
|
||||
}
|
||||
|
||||
public function delete(PaymentMethod $method): void
|
||||
{
|
||||
$snapshot = $method->only([
|
||||
'id', 'type', 'name', 'driver', 'capture_mode',
|
||||
'captured_status', 'authorized_status', 'position', 'enabled',
|
||||
]);
|
||||
|
||||
$method->delete();
|
||||
|
||||
$this->cache->forget();
|
||||
|
||||
Event::dispatch(new PaymentMethodDeleted($snapshot));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user