Use WooCommerce store currency for product and order money.
Deploy Ladill Woo Manager / deploy (push) Successful in 35s
Deploy Ladill Woo Manager / deploy (push) Successful in 35s
Sync currency from the plugin, format product prices and totals with it, and show the store currency on product price fields instead of hard-coding GHS.
This commit is contained in:
@@ -65,8 +65,13 @@ class WooOrder extends Model
|
||||
public function totalFormatted(): string
|
||||
{
|
||||
$minor = (int) $this->total_minor;
|
||||
$currency = strtoupper((string) ($this->currency ?: 'GHS'));
|
||||
$currency = strtoupper(trim((string) ($this->currency ?: '')));
|
||||
if ($currency === '' && ($this->relationLoaded('store') || $this->store)) {
|
||||
$currency = $this->store->currency();
|
||||
}
|
||||
|
||||
return $currency.' '.number_format($minor / 100, 2);
|
||||
$amount = number_format($minor / 100, 2);
|
||||
|
||||
return $currency !== '' ? $currency.' '.$amount : $amount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,9 +64,24 @@ class WooProduct extends Model
|
||||
return '—';
|
||||
}
|
||||
|
||||
$currency = strtoupper((string) ($this->store?->metadata['currency'] ?? 'GHS'));
|
||||
if ($this->relationLoaded('store') || $this->store) {
|
||||
return $this->store->formatMoney($minor);
|
||||
}
|
||||
|
||||
return $currency.' '.number_format($minor / 100, 2);
|
||||
return number_format($minor / 100, 2);
|
||||
}
|
||||
|
||||
public function regularPriceFormatted(): string
|
||||
{
|
||||
if ($this->regular_price_minor === null) {
|
||||
return '—';
|
||||
}
|
||||
|
||||
if ($this->relationLoaded('store') || $this->store) {
|
||||
return $this->store->formatMoney((int) $this->regular_price_minor);
|
||||
}
|
||||
|
||||
return number_format(((int) $this->regular_price_minor) / 100, 2);
|
||||
}
|
||||
|
||||
public function statusLabel(): string
|
||||
|
||||
@@ -70,4 +70,39 @@ class WooStore extends Model
|
||||
{
|
||||
return rtrim((string) config('app.url'), '/').'/webhooks/woocommerce/'.$this->public_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* WooCommerce store currency (ISO 4217), synced from the connected site.
|
||||
* Does not fall back to GHS — unknown currency is empty until the next sync.
|
||||
*/
|
||||
public function currency(): string
|
||||
{
|
||||
$meta = is_array($this->metadata) ? $this->metadata : [];
|
||||
$currency = strtoupper(trim((string) ($meta['currency'] ?? '')));
|
||||
|
||||
return preg_match('/^[A-Z]{3}$/', $currency) === 1 ? $currency : '';
|
||||
}
|
||||
|
||||
public function currencySymbol(): string
|
||||
{
|
||||
$meta = is_array($this->metadata) ? $this->metadata : [];
|
||||
$symbol = trim((string) ($meta['currency_symbol'] ?? ''));
|
||||
if ($symbol !== '') {
|
||||
return $symbol;
|
||||
}
|
||||
|
||||
return $this->currency();
|
||||
}
|
||||
|
||||
public function formatMoney(int|float|null $minor): string
|
||||
{
|
||||
if ($minor === null) {
|
||||
return '—';
|
||||
}
|
||||
|
||||
$currency = $this->currency();
|
||||
$amount = number_format(((int) $minor) / 100, 2);
|
||||
|
||||
return $currency !== '' ? $currency.' '.$amount : $amount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,53 @@ class CatalogSyncService
|
||||
|
||||
public function syncAll(WooStore $store, ?User $user = null): array
|
||||
{
|
||||
$this->syncStoreMeta($store);
|
||||
|
||||
return [
|
||||
'categories' => $this->syncCategories($store),
|
||||
'products' => $this->syncProducts($store, $user),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull store-level settings (currency, site name) from the WooCommerce plugin.
|
||||
*/
|
||||
public function syncStoreMeta(WooStore $store): void
|
||||
{
|
||||
$response = $this->client->get($store, 'store');
|
||||
if (! is_array($response)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->applyStoreMeta($store, $response);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $payload */
|
||||
public function applyStoreMeta(WooStore $store, array $payload): void
|
||||
{
|
||||
$currency = strtoupper(trim((string) ($payload['currency'] ?? '')));
|
||||
if (preg_match('/^[A-Z]{3}$/', $currency) !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$meta = is_array($store->metadata) ? $store->metadata : [];
|
||||
$meta['currency'] = $currency;
|
||||
|
||||
$symbol = trim((string) ($payload['currency_symbol'] ?? ''));
|
||||
if ($symbol !== '') {
|
||||
$meta['currency_symbol'] = $symbol;
|
||||
}
|
||||
|
||||
$siteName = trim((string) ($payload['site_name'] ?? ''));
|
||||
$updates = ['metadata' => $meta];
|
||||
if ($siteName !== '' && (string) $store->site_name !== $siteName) {
|
||||
$updates['site_name'] = $siteName;
|
||||
}
|
||||
|
||||
$store->forceFill($updates)->save();
|
||||
$store->refresh();
|
||||
}
|
||||
|
||||
public function syncCategories(WooStore $store): int
|
||||
{
|
||||
$response = $this->client->get($store, 'categories', ['per_page' => 100]);
|
||||
@@ -53,11 +94,20 @@ class CatalogSyncService
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (isset($response['currency']) || isset($response['currency_symbol'])) {
|
||||
$this->applyStoreMeta($store, $response);
|
||||
}
|
||||
|
||||
$items = $response['data'] ?? $response;
|
||||
if (! is_array($items)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Bare list responses are numeric arrays of products — ignore non-product keys.
|
||||
if (array_is_list($items) === false && isset($response['data']) === false) {
|
||||
$items = array_values(array_filter($items, fn ($item) => is_array($item) && isset($item['id'])));
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
foreach ($items as $item) {
|
||||
if (! is_array($item)) {
|
||||
|
||||
@@ -30,7 +30,13 @@ class OrderIngestService
|
||||
->all();
|
||||
|
||||
$totalMinor = (int) round(((float) ($payload['total'] ?? 0)) * 100);
|
||||
$currency = strtoupper((string) ($payload['currency'] ?? 'GHS'));
|
||||
$currency = strtoupper(trim((string) ($payload['currency'] ?? '')));
|
||||
if (preg_match('/^[A-Z]{3}$/', $currency) !== 1) {
|
||||
$currency = $store->currency();
|
||||
}
|
||||
if ($currency === '') {
|
||||
$currency = 'XXX';
|
||||
}
|
||||
|
||||
$wcUpdatedAt = isset($payload['date_modified_gmt'])
|
||||
? Carbon::parse($payload['date_modified_gmt'].' UTC')
|
||||
|
||||
@@ -9,6 +9,7 @@ class OrderSyncService
|
||||
public function __construct(
|
||||
private PluginClient $client,
|
||||
private OrderIngestService $orders,
|
||||
private CatalogSyncService $catalog,
|
||||
) {}
|
||||
|
||||
public function syncOrders(WooStore $store): int
|
||||
@@ -18,11 +19,19 @@ class OrderSyncService
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (isset($response['currency']) || isset($response['currency_symbol'])) {
|
||||
$this->catalog->applyStoreMeta($store, $response);
|
||||
}
|
||||
|
||||
$items = $response['data'] ?? $response;
|
||||
if (! is_array($items)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (array_is_list($items) === false && isset($response['data']) === false) {
|
||||
$items = array_values(array_filter($items, fn ($item) => is_array($item) && (isset($item['id']) || isset($item['number']))));
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
foreach ($items as $item) {
|
||||
if (! is_array($item)) {
|
||||
|
||||
Binary file not shown.
@@ -40,16 +40,37 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$storeCurrency = $currentStore->currency();
|
||||
$storeCurrencyLabel = $storeCurrency !== '' ? $storeCurrency : 'store currency';
|
||||
@endphp
|
||||
<div class="mt-4 grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Regular price</label>
|
||||
<input type="number" step="0.01" min="0" name="regular_price" value="{{ old('regular_price', $product->regular_price_minor !== null ? $product->regular_price_minor / 100 : '') }}" class="mt-1 w-full rounded-xl border-slate-200 text-sm">
|
||||
<label class="block text-sm font-medium text-slate-700">Regular price ({{ $storeCurrencyLabel }})</label>
|
||||
<div class="relative mt-1">
|
||||
@if($storeCurrency !== '')
|
||||
<span class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-xs font-medium text-slate-400">{{ $storeCurrency }}</span>
|
||||
@endif
|
||||
<input type="number" step="0.01" min="0" name="regular_price"
|
||||
value="{{ old('regular_price', $product->regular_price_minor !== null ? $product->regular_price_minor / 100 : '') }}"
|
||||
class="w-full rounded-xl border-slate-200 text-sm {{ $storeCurrency !== '' ? 'pl-14' : '' }}">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Sale price</label>
|
||||
<input type="number" step="0.01" min="0" name="sale_price" value="{{ old('sale_price', $product->sale_price_minor !== null ? $product->sale_price_minor / 100 : '') }}" class="mt-1 w-full rounded-xl border-slate-200 text-sm">
|
||||
<label class="block text-sm font-medium text-slate-700">Sale price ({{ $storeCurrencyLabel }})</label>
|
||||
<div class="relative mt-1">
|
||||
@if($storeCurrency !== '')
|
||||
<span class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-xs font-medium text-slate-400">{{ $storeCurrency }}</span>
|
||||
@endif
|
||||
<input type="number" step="0.01" min="0" name="sale_price"
|
||||
value="{{ old('sale_price', $product->sale_price_minor !== null ? $product->sale_price_minor / 100 : '') }}"
|
||||
class="w-full rounded-xl border-slate-200 text-sm {{ $storeCurrency !== '' ? 'pl-14' : '' }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if($storeCurrency === '')
|
||||
<p class="mt-2 text-xs text-amber-700">Store currency not synced yet. Run <strong>Sync from WooCommerce</strong> on the products page so prices use your WooCommerce currency.</p>
|
||||
@endif
|
||||
|
||||
<div class="mt-4 grid gap-4 sm:grid-cols-2">
|
||||
<label class="flex items-center gap-2 text-sm text-slate-700">
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\WooProduct;
|
||||
use App\Models\WooStore;
|
||||
use App\Services\Woo\CatalogSyncService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class WooStoreCurrencyTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_product_price_uses_store_woocommerce_currency_not_ghs_default(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$store = WooStore::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'site_url' => 'https://shop.example.com',
|
||||
'site_name' => 'Example Shop',
|
||||
'status' => WooStore::STATUS_ACTIVE,
|
||||
'metadata' => [
|
||||
'currency' => 'USD',
|
||||
'currency_symbol' => '$',
|
||||
],
|
||||
]);
|
||||
|
||||
$product = WooProduct::query()->create([
|
||||
'woo_store_id' => $store->id,
|
||||
'external_id' => 42,
|
||||
'name' => 'Notebook',
|
||||
'slug' => 'notebook',
|
||||
'status' => WooProduct::STATUS_PUBLISH,
|
||||
'type' => 'simple',
|
||||
'regular_price_minor' => 1999,
|
||||
]);
|
||||
$product->setRelation('store', $store);
|
||||
|
||||
$this->assertSame('USD', $store->currency());
|
||||
$this->assertSame('USD 19.99', $product->priceFormatted());
|
||||
$this->assertStringNotContainsString('GHS', $product->priceFormatted());
|
||||
}
|
||||
|
||||
public function test_apply_store_meta_persists_woocommerce_currency(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$store = WooStore::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'site_url' => 'https://shop.example.com',
|
||||
'site_name' => 'Old name',
|
||||
'status' => WooStore::STATUS_ACTIVE,
|
||||
'metadata' => null,
|
||||
]);
|
||||
|
||||
app(CatalogSyncService::class)->applyStoreMeta($store, [
|
||||
'currency' => 'eur',
|
||||
'currency_symbol' => '€',
|
||||
'site_name' => 'Euro Shop',
|
||||
]);
|
||||
|
||||
$store->refresh();
|
||||
$this->assertSame('EUR', $store->currency());
|
||||
$this->assertSame('€', $store->currencySymbol());
|
||||
$this->assertSame('Euro Shop', $store->site_name);
|
||||
$this->assertSame('EUR 10.00', $store->formatMoney(1000));
|
||||
}
|
||||
|
||||
public function test_unknown_currency_does_not_fall_back_to_ghs(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$store = WooStore::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'site_url' => 'https://shop.example.com',
|
||||
'status' => WooStore::STATUS_ACTIVE,
|
||||
'metadata' => [],
|
||||
]);
|
||||
|
||||
$this->assertSame('', $store->currency());
|
||||
$this->assertSame('12.50', $store->formatMoney(1250));
|
||||
$this->assertStringNotContainsString('GHS', $store->formatMoney(1250));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user