- {{ $page->t("While public pricing is not yet available, our team can help you understand the best option for your organization.") }} -
- - {{ $page->t("Talk to our team") }} - -{{ $page->t("For organizations that need support, guidance, and a tailored LibreSign adoption path.") }}
-{{ $page->t("For organizations that want to integrate LibreSign with existing systems and workflows.") }}
+ @if (!empty($cardHighlights)) +{{ $page->t('No featured products are available right now. Please contact our team for current commercial options.') }}
{{ $page->t("For organizations interested in hosted or managed infrastructure options when available.") }}
+ @if ($comparisonFeatureGroups->isNotEmpty() || $detailRows->isNotEmpty()) +Short description
', + 'permalink' => 'https://account.example.test/product/basic/', + 'type' => 'simple', + 'is_purchasable' => true, + 'has_options' => false, + 'prices' => [ + 'currency_prefix' => 'R$ ', + 'currency_suffix' => '', + 'currency_minor_unit' => 2, + 'currency_decimal_separator' => ',', + 'currency_thousand_separator' => '.', + 'price' => '5500', + 'price_range' => ['min_amount' => '5500'], + ], + 'add_to_cart' => [ + 'text' => 'View product', + 'single_text' => 'View product', + ], + 'attributes' => [ + [ + 'name' => 'Storage', + 'options' => ['1 Gb'], + 'visible' => true, + ], + ], + ], + 'https://account.example.test/wp-json/wc/v3/products/10?_fields=id,attributes' => [ + 'attributes' => [ + [ + 'name' => 'Storage', + 'options' => ['2 Gb'], + 'visible' => true, + ], + [ + 'name' => 'pricing_card_colors', + 'options' => ['background:#EBF7F2', 'button_text:#FFFFFF'], + 'visible' => true, + ], + ], + ], + 'https://account.example.test/wp-json/wc/store/v1/products/11' => [ + 'name' => 'Básico', + 'short_description' => 'Descrição curta
', + 'permalink' => 'https://account.example.test/product/basic-pt/', + 'type' => 'simple', + 'is_purchasable' => true, + 'has_options' => false, + 'prices' => [ + 'currency_prefix' => 'R$ ', + 'currency_suffix' => '', + 'currency_minor_unit' => 2, + 'currency_decimal_separator' => ',', + 'currency_thousand_separator' => '.', + 'price' => '5500', + 'price_range' => ['min_amount' => '5500'], + ], + 'add_to_cart' => [ + 'text' => 'Ver produto', + 'single_text' => 'Ver produto', + ], + ], + 'https://account.example.test/wp-json/wc/v3/products/11?_fields=id,attributes' => [ + 'attributes' => [], + ], + ], + [ + 'https://account.example.test/wp-json/pll/v1/languages' => json_encode([ + ['slug' => 'en', 'w3c' => 'en-US'], + ['slug' => 'pt-br', 'w3c' => 'pt-BR'], + ]), + ], + ['Authorization: Basic test'], + new WooCommerceProductTransformer(), + ); + + $items = $collection->items($page); + + self::assertCount(2, $items); + self::assertSame('Basic', $items[0]['title']); + self::assertSame('2 Gb', $items[0]['attributes'][0]['values'][0]); + self::assertSame([ + 'background' => '#EBF7F2', + 'button_text' => '#FFFFFF', + ], $items[0]['pricingCardColors']); + self::assertSame('Básico', $items[1]['title']); + self::assertSame('pt-BR', $items[1]['lang']); + } +} + +final class FakeWooCommerceProductCollection extends WooCommerceProductCollection +{ + public function __construct( + private readonly array $jsonResponses, + private readonly array $contentResponses, + array $authenticatedHeaders = [], + ?WooCommerceProductTransformer $transformer = null, + ) { + parent::__construct($authenticatedHeaders, 15, $transformer); + } + + protected function fetchJson(string $url, array $headers = []): ?array + { + return $this->jsonResponses[$url] ?? null; + } + + protected function fetchContent(string $url, array $headers = []): string|false + { + return $this->contentResponses[$url] ?? false; + } +} + +final class FakeJigsawPage +{ + public function __construct(private readonly array $config) + { + } + + public function get(string $key) + { + return $this->config[$key] ?? null; + } +} diff --git a/tests/Unit/Support/Pricing/WooCommerceProductTransformerTest.php b/tests/Unit/Support/Pricing/WooCommerceProductTransformerTest.php new file mode 100644 index 00000000..0209ebff --- /dev/null +++ b/tests/Unit/Support/Pricing/WooCommerceProductTransformerTest.php @@ -0,0 +1,238 @@ +transformer = new WooCommerceProductTransformer(); + } + + #[DataProvider('formatWooCommercePriceProvider')] + public function testFormatWooCommercePrice(array $prices, ?string $amount, ?string $expected): void + { + self::assertSame($expected, $this->transformer->formatWooCommercePrice($prices, $amount)); + } + + public static function formatWooCommercePriceProvider(): iterable + { + yield 'null amount returns null' => [ + ['currency_prefix' => 'R$ ', 'currency_minor_unit' => 2], + null, + null, + ]; + + yield 'formats brazilian currency' => [ + [ + 'currency_prefix' => 'R$ ', + 'currency_suffix' => '', + 'currency_minor_unit' => 2, + 'currency_decimal_separator' => ',', + 'currency_thousand_separator' => '.', + ], + '5500', + 'R$ 55,00', + ]; + + yield 'formats amount without minor unit' => [ + [ + 'currency_prefix' => '', + 'currency_suffix' => ' credits', + 'currency_minor_unit' => 0, + 'currency_decimal_separator' => ',', + 'currency_thousand_separator' => '.', + ], + '1200', + '1.200 credits', + ]; + } + + #[DataProvider('normalizeWooCommerceAttributeProvider')] + public function testNormalizeWooCommerceAttribute(array $attribute, ?array $expected): void + { + self::assertSame($expected, $this->transformer->normalizeWooCommerceAttribute($attribute)); + } + + public static function normalizeWooCommerceAttributeProvider(): iterable + { + yield 'uses terms when available' => [ + [ + 'name' => 'Storage', + 'terms' => [ + ['name' => '2 Gb'], + ['name' => '120 Gb'], + ], + 'visible' => true, + ], + [ + 'name' => 'Storage', + 'values' => ['2 Gb', '120 Gb'], + 'visible' => true, + ], + ]; + + yield 'falls back to string options' => [ + [ + 'name' => 'Term length', + 'options' => ['monthly', 'yearly'], + 'visible' => false, + ], + [ + 'name' => 'Term length', + 'values' => ['monthly', 'yearly'], + 'visible' => false, + ], + ]; + + yield 'returns null for empty values' => [ + [ + 'name' => 'Empty', + 'terms' => [], + 'options' => [' ', null], + ], + null, + ]; + } + + #[DataProvider('parsePricingCardColorsProvider')] + public function testParsePricingCardColors(array $attributes, array $expected): void + { + self::assertSame($expected, $this->transformer->parsePricingCardColors($attributes)); + } + + public static function parsePricingCardColorsProvider(): iterable + { + yield 'parses valid pricing colors' => [ + [ + [ + 'name' => 'Pricing Card Colors', + 'values' => [ + 'background:#0f6e56', + 'button_text:#ffffff', + 'border:D4F0E4', + ], + ], + ], + [ + 'background' => '#0F6E56', + 'button_text' => '#FFFFFF', + 'border' => '#D4F0E4', + ], + ]; + + yield 'ignores invalid color definitions' => [ + [ + [ + 'name' => 'pricing_card_colors', + 'values' => [ + 'background:not-a-color', + 'accent:', + 'check:#12345G', + ], + ], + ], + [], + ]; + + yield 'ignores unrelated attributes' => [ + [ + [ + 'name' => 'Storage', + 'values' => ['2 Gb'], + ], + ], + [], + ]; + } + + public function testMapProductBuildsExpectedPayloadWithoutMocks(): void + { + $fromApi = [ + 'id' => 10, + 'slug' => 'basic', + 'date' => '2026-07-03T12:00:00', + 'lang' => 'en', + 'translations' => ['en' => 10, 'pt-br' => 11], + 'link' => 'https://account.example.test/product/basic/', + 'title' => ['rendered' => 'Basic fallback'], + ]; + $productDetails = [ + 'name' => 'Basic', + 'short_description' => 'Short description
', + 'permalink' => 'https://account.example.test/product/basic/', + 'type' => 'simple', + 'is_purchasable' => true, + 'has_options' => false, + 'prices' => [ + 'currency_prefix' => 'R$ ', + 'currency_suffix' => '', + 'currency_minor_unit' => 2, + 'currency_decimal_separator' => ',', + 'currency_thousand_separator' => '.', + 'price' => '5500', + 'price_range' => ['min_amount' => '5500'], + ], + 'add_to_cart' => [ + 'text' => 'View product', + 'single_text' => 'View product', + ], + 'attributes' => [ + [ + 'name' => 'Storage', + 'options' => ['2 Gb'], + 'visible' => true, + ], + [ + 'name' => 'pricing_card_colors', + 'options' => ['background:#EBF7F2', 'button_text:#FFFFFF'], + 'visible' => true, + ], + ], + ]; + $authenticatedProductDetails = []; + $wordPressLanguages = [ + (object) ['slug' => 'en', 'w3c' => 'en-US'], + ]; + + $result = $this->transformer->mapProduct( + $fromApi, + $productDetails, + $authenticatedProductDetails, + $wordPressLanguages, + ); + + self::assertSame('Basic', $result['title']); + self::assertSame('10-11', $result['translationGroup']); + self::assertSame('R$ 55,00', $result['price']); + self::assertTrue($result['hasPriceRange']); + self::assertSame('en-US', $result['lang']); + self::assertSame('Short description
', $result['description']); + self::assertSame([ + 'background' => '#EBF7F2', + 'button_text' => '#FFFFFF', + ], $result['pricingCardColors']); + self::assertSame([ + [ + 'name' => 'Storage', + 'values' => ['2 Gb'], + 'visible' => true, + ], + [ + 'name' => 'pricing_card_colors', + 'values' => ['background:#EBF7F2', 'button_text:#FFFFFF'], + 'visible' => true, + ], + ], $result['attributes']); + } +} \ No newline at end of file