Use WooCommerce store currency for product and order money.
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:
isaacclad
2026-07-24 15:38:07 +00:00
parent f7eed690ab
commit e709de6593
9 changed files with 234 additions and 9 deletions
+7 -2
View File
@@ -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;
}
}
+17 -2
View File
@@ -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
+35
View File
@@ -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;
}
}
+50
View File
@@ -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)) {
+7 -1
View File
@@ -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
View File
@@ -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.
+25 -4
View File
@@ -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">
+84
View File
@@ -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));
}
}