PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.6.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.6.2
4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.5.2 All 199 releases
betterdocs / includes / Core / StoreFaqContent.php

StoreFaqContent.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.6.2, at includes/Core/StoreFaqContent.php

516 lines 21.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPDeveloper\BetterDocs\Core;
4
5 /**
6 * StoreFaqContent — deterministic, store-aware Product FAQ generator.
7 *
8 * Single source of truth for the WooCommerce "sample Product FAQs" starter set:
9 * four groups (Payments & Billing, Shipping & Delivery, Returns & Refunds,
10 * Orders & Account) of three questions each, curated to cover the highest
11 * friction pre-/post-purchase buyer questions.
12 *
13 * It serves two callers:
14 * - SampleDocs::suggested_categories() uses definition() for the chips/titles
15 * shown on the "detected profile" screen.
16 * - SampleDocs::generate() uses generate() as the hybrid fallback when the AI
17 * proxy is unavailable (offline / quota / no model), producing real answers
18 * grounded in the site's actual store config (payment gateways, currency,
19 * tax, shipping regions, return window, policy pages) — no AI required.
20 *
21 * Answers degrade gracefully: when a datum is missing, the copy falls back to a
22 * generic-but-accurate sentence rather than inventing specifics.
23 *
24 * @since 4.5.3
25 */
26 class StoreFaqContent {
27 /**
28 * The curated group + question structure. Titles are static; answers are
29 * filled per-site in generate().
30 *
31 * @return array
32 */
33 public function definition() {
34 return [
35 [
36 'name' => __( 'Payments & Billing', 'betterdocs' ),
37 'slug' => 'payments-billing',
38 'icon' => 'help',
39 'description' => __( 'How customers can pay and what to expect when checking out.', 'betterdocs' ),
40 'questions' => [
41 'payment_methods' => __( 'What payment methods do you accept?', 'betterdocs' ),
42 'payment_security' => __( 'Is it safe to enter my card details at checkout?', 'betterdocs' ),
43 'currency_tax' => __( 'What currency are prices in, and will I be charged tax?', 'betterdocs' ),
44 ],
45 ],
46 [
47 'name' => __( 'Shipping & Delivery', 'betterdocs' ),
48 'slug' => 'shipping-delivery',
49 'icon' => 'truck',
50 'description' => __( 'Where you ship, how long it takes, and how to track an order.', 'betterdocs' ),
51 'questions' => [
52 'shipping_coverage' => __( 'Where do you ship and how long does delivery take?', 'betterdocs' ),
53 'shipping_cost' => __( 'How much does shipping cost?', 'betterdocs' ),
54 'order_tracking' => __( 'How can I track my order?', 'betterdocs' ),
55 ],
56 ],
57 [
58 'name' => __( 'Returns & Refunds', 'betterdocs' ),
59 'slug' => 'returns-refunds',
60 'icon' => 'refund',
61 'description' => __( 'Your return window, how to request a refund, and what to expect.', 'betterdocs' ),
62 'questions' => [
63 'return_policy' => __( 'What is your return & refund policy?', 'betterdocs' ),
64 'return_how' => __( 'How do I request a return or refund?', 'betterdocs' ),
65 'refund_timing' => __( 'How long do refunds take, and who pays return shipping?', 'betterdocs' ),
66 ],
67 ],
68 [
69 'name' => __( 'Orders & Account', 'betterdocs' ),
70 'slug' => 'orders-account',
71 'icon' => 'book',
72 'description' => __( 'Placing, changing, and tracking orders, and whether an account is needed.', 'betterdocs' ),
73 'questions' => [
74 'place_order' => __( 'How do I place an order?', 'betterdocs' ),
75 'modify_cancel' => __( 'Can I change or cancel my order after checkout?', 'betterdocs' ),
76 'account_status' => __( 'Do I need an account to buy, and how do I check order status?', 'betterdocs' ),
77 ],
78 ],
79 ];
80 }
81
82 /**
83 * Build the full categories array (proxy-shaped) with real store answers.
84 *
85 * @param array $profile The SiteProfiler profile (uses ['woocommerce'] + ['site']).
86 * @return array [{ name, slug, description, articles:[{ title, content_html, excerpt }] }]
87 */
88 public function generate( array $profile ) {
89 $woo = isset( $profile['woocommerce'] ) && is_array( $profile['woocommerce'] ) ? $profile['woocommerce'] : [];
90 $site = isset( $profile['site'] ) && is_array( $profile['site'] ) ? $profile['site'] : [];
91
92 $categories = [];
93 foreach ( $this->definition() as $group ) {
94 $articles = [];
95 foreach ( $group['questions'] as $id => $title ) {
96 $html = $this->answer_html( $id, $woo, $site );
97 $articles[] = [
98 'title' => $title,
99 'content_html' => $html,
100 'excerpt' => $this->excerpt( $html ),
101 ];
102 }
103
104 $categories[] = [
105 'name' => $group['name'],
106 'slug' => $group['slug'],
107 'description' => $group['description'],
108 'articles' => $articles,
109 ];
110 }
111
112 return $categories;
113 }
114
115 /**
116 * The single "store-wide" FAQ group — the questions every shopper asks on any
117 * product page, regardless of what the product is (payments, shipping, returns,
118 * orders). One group rather than the four themed ones from definition(): it is
119 * flagged "show on all products", so it appears once on every product page, and a
120 * dozen questions in one accordion is a lot. A curated set of six spanning all four
121 * themes reads best.
122 *
123 * @return array { name, slug, icon, description, questions:[ id => title ] }
124 */
125 public function consolidated_definition() {
126 return [
127 'name' => __( 'Shipping, Returns & Payments', 'betterdocs' ),
128 'slug' => 'store-info',
129 'icon' => 'truck',
130 'description' => __( 'Common questions about payments, delivery, returns and orders — shown on every product.', 'betterdocs' ),
131 'questions' => [
132 'payment_methods' => __( 'What payment methods do you accept?', 'betterdocs' ),
133 'shipping_coverage' => __( 'Where do you ship and how long does delivery take?', 'betterdocs' ),
134 'shipping_cost' => __( 'How much does shipping cost?', 'betterdocs' ),
135 'order_tracking' => __( 'How can I track my order?', 'betterdocs' ),
136 'return_policy' => __( 'What is your return & refund policy?', 'betterdocs' ),
137 'modify_cancel' => __( 'Can I change or cancel my order after checkout?', 'betterdocs' ),
138 ],
139 ];
140 }
141
142 /**
143 * Build the single store-wide group (proxy-shaped) with real, settings-grounded
144 * answers — the deterministic Layer 1 of the WooCommerce product FAQ. Reuses the
145 * same per-question answer builders generate() uses.
146 *
147 * @param array $profile The SiteProfiler profile (uses ['woocommerce'] + ['site']).
148 * @return array { name, slug, description, articles:[{ title, content_html, excerpt }] }
149 */
150 public function generate_consolidated( array $profile ) {
151 $woo = isset( $profile['woocommerce'] ) && is_array( $profile['woocommerce'] ) ? $profile['woocommerce'] : [];
152 $site = isset( $profile['site'] ) && is_array( $profile['site'] ) ? $profile['site'] : [];
153 $group = $this->consolidated_definition();
154
155 $articles = [];
156 foreach ( $group['questions'] as $id => $title ) {
157 $html = $this->answer_html( $id, $woo, $site );
158 $articles[] = [
159 'title' => $title,
160 'content_html' => $html,
161 'excerpt' => $this->excerpt( $html ),
162 ];
163 }
164
165 return [
166 'name' => $group['name'],
167 'slug' => $group['slug'],
168 'icon' => $group['icon'],
169 'description' => $group['description'],
170 'articles' => $articles,
171 ];
172 }
173
174 /* --------------------------------------------------------------------- */
175 /* Answer builders */
176 /* --------------------------------------------------------------------- */
177
178 /**
179 * @return string Safe HTML (paragraphs/lists/links) for one answer.
180 */
181 protected function answer_html( $id, array $woo, array $site ) {
182 switch ( $id ) {
183 case 'payment_methods':
184 return $this->answer_payment_methods( $woo );
185 case 'payment_security':
186 return $this->answer_payment_security( $site, $woo );
187 case 'currency_tax':
188 return $this->answer_currency_tax( $woo );
189 case 'shipping_coverage':
190 return $this->answer_shipping_coverage( $woo );
191 case 'shipping_cost':
192 return $this->answer_shipping_cost( $woo );
193 case 'order_tracking':
194 return $this->answer_order_tracking( $woo );
195 case 'return_policy':
196 return $this->answer_return_policy( $woo );
197 case 'return_how':
198 return $this->answer_return_how( $woo );
199 case 'refund_timing':
200 return $this->answer_refund_timing( $woo );
201 case 'place_order':
202 return $this->answer_place_order( $woo );
203 case 'modify_cancel':
204 return $this->answer_modify_cancel();
205 case 'account_status':
206 return $this->answer_account_status( $woo );
207 }
208
209 return '';
210 }
211
212 protected function answer_payment_methods( array $woo ) {
213 $methods = isset( $woo['payment_methods'] ) ? (array) $woo['payment_methods'] : [];
214
215 if ( ! empty( $methods ) ) {
216 $intro = sprintf(
217 /* translators: %s: list of payment method names. */
218 __( 'We accept %s.', 'betterdocs' ),
219 $this->human_list( $methods )
220 );
221 } else {
222 $intro = __( 'We accept all major payment methods, including popular credit and debit cards and digital wallets.', 'betterdocs' );
223 }
224
225 $second = __( 'You can review every available option on the checkout page before you place your order, and all payments are processed securely.', 'betterdocs' );
226
227 return $this->p( $intro ) . $this->p( $second );
228 }
229
230 protected function answer_payment_security( array $site, array $woo = [] ) {
231 $name = isset( $site['title'] ) && '' !== $site['title'] ? $site['title'] : __( 'our store', 'betterdocs' );
232
233 $first = __( 'Yes. Your payment is processed over a secure, encrypted (SSL) connection.', 'betterdocs' );
234 $second = sprintf(
235 /* translators: %s: site/store name. */
236 __( 'Card details are handled by our trusted payment provider — %s never stores your full card number on its own servers.', 'betterdocs' ),
237 $name
238 );
239
240 $privacy = isset( $woo['privacy_page_url'] ) ? (string) $woo['privacy_page_url'] : '';
241 $third = '';
242 if ( '' !== $privacy ) {
243 $third = $this->p(
244 sprintf(
245 /* translators: %s: link to the store's privacy policy page. */
246 __( 'See our %s for how your information is collected and used.', 'betterdocs' ),
247 '<a href="' . esc_url( $privacy ) . '">' . esc_html__( 'privacy policy', 'betterdocs' ) . '</a>'
248 )
249 );
250 }
251
252 return $this->p( $first ) . $this->p( $second ) . $third;
253 }
254
255 protected function answer_currency_tax( array $woo ) {
256 $currency = isset( $woo['currency'] ) ? (string) $woo['currency'] : '';
257 $symbol = isset( $woo['currency_symbol'] ) ? (string) $woo['currency_symbol'] : '';
258
259 if ( '' !== $currency ) {
260 $label = '' !== $symbol ? sprintf( '%s (%s)', $currency, $symbol ) : $currency;
261 $first = sprintf(
262 /* translators: %s: currency code (and symbol). */
263 __( 'All prices on our store are shown in %s.', 'betterdocs' ),
264 $label
265 );
266 } else {
267 $first = __( 'All prices are shown in our store currency, displayed throughout checkout.', 'betterdocs' );
268 }
269
270 if ( ! empty( $woo['tax_enabled'] ) ) {
271 $second = ! empty( $woo['prices_include_tax'] )
272 ? __( 'Applicable taxes are already included in the price you see.', 'betterdocs' )
273 : __( 'Any applicable taxes are calculated based on your location and shown at checkout before you pay.', 'betterdocs' );
274 } else {
275 $second = __( 'The total you see at checkout is the final amount you pay.', 'betterdocs' );
276 }
277
278 return $this->p( $first ) . $this->p( $second );
279 }
280
281 protected function answer_shipping_coverage( array $woo ) {
282 $regions = isset( $woo['shipping_regions'] ) ? (array) $woo['shipping_regions'] : [];
283 $location = isset( $woo['store_location'] ) ? (string) $woo['store_location'] : '';
284
285 if ( ! empty( $regions ) ) {
286 $first = sprintf(
287 /* translators: %s: list of shipping region names. */
288 __( 'We currently ship to %s.', 'betterdocs' ),
289 $this->human_list( $regions )
290 );
291 } else {
292 $first = __( 'We ship to all the destinations available at checkout — enter your address to see the options for your area.', 'betterdocs' );
293 }
294
295 $second = __( 'Most orders are processed within 1–2 business days. Delivery time then depends on your destination and the shipping method you choose at checkout.', 'betterdocs' );
296
297 if ( '' !== $location ) {
298 $second .= ' ' . sprintf(
299 /* translators: %s: store base location. */
300 __( 'Orders ship from %s.', 'betterdocs' ),
301 $location
302 );
303 }
304
305 return $this->p( $first ) . $this->p( $second );
306 }
307
308 protected function answer_shipping_cost( array $woo = [] ) {
309 $first = __( 'Shipping cost is calculated automatically at checkout based on your delivery address and the method you select, so you always see the exact amount before you pay.', 'betterdocs' );
310
311 $free_min = isset( $woo['free_shipping_min'] ) ? (float) $woo['free_shipping_min'] : 0;
312 if ( $free_min > 0 ) {
313 $first .= ' ' . sprintf(
314 /* translators: %s: formatted minimum order amount for free shipping. */
315 __( 'Orders over %s qualify for free shipping.', 'betterdocs' ),
316 $this->format_amount( $free_min, $woo )
317 );
318 }
319
320 return $this->p( $first )
321 . $this->p( __( 'Add your items to the cart and enter your address to view the available shipping rates for your order.', 'betterdocs' ) );
322 }
323
324 protected function answer_order_tracking( array $woo = [] ) {
325 $first = __( 'As soon as your order ships, we email you a confirmation with the details you need to follow its progress.', 'betterdocs' );
326 $account = isset( $woo['account_page_url'] ) ? (string) $woo['account_page_url'] : '';
327
328 if ( '' !== $account ) {
329 $second = sprintf(
330 /* translators: %s: link to the customer's account orders page. */
331 __( 'If you have an account, you can also track every order any time from your %s.', 'betterdocs' ),
332 '<a href="' . esc_url( $account ) . '">' . esc_html__( 'account\'s Orders page', 'betterdocs' ) . '</a>'
333 );
334 } else {
335 $second = __( 'If you have an account, you can also see the current status of every order any time from your account\'s Orders page.', 'betterdocs' );
336 }
337
338 return $this->p( $first ) . $this->p( $second );
339 }
340
341 protected function answer_return_policy( array $woo ) {
342 $window = isset( $woo['return_window'] ) ? (int) $woo['return_window'] : 0;
343
344 if ( $window > 0 ) {
345 $first = sprintf(
346 /* translators: %d: number of days in the return window. */
347 _n(
348 'You can request a return or refund within %d day of receiving your order, as long as the item is unused and in its original condition.',
349 'You can request a return or refund within %d days of receiving your order, as long as the item is unused and in its original condition.',
350 $window,
351 'betterdocs'
352 ),
353 $window
354 );
355 } else {
356 $first = __( 'If you are not completely happy with your purchase, you can request a return or refund — just get in touch and we will make it right.', 'betterdocs' );
357 }
358
359 return $this->p( $first ) . $this->policy_line( $woo );
360 }
361
362 protected function answer_return_how( array $woo = [] ) {
363 $steps = '<ol>'
364 . '<li>' . esc_html__( 'Contact us with your order number and the item you would like to return.', 'betterdocs' ) . '</li>'
365 . '<li>' . esc_html__( 'We confirm eligibility and send you return instructions.', 'betterdocs' ) . '</li>'
366 . '<li>' . esc_html__( 'Pack the item securely and ship it back to the address we provide.', 'betterdocs' ) . '</li>'
367 . '<li>' . esc_html__( 'Once it arrives, we process your refund or exchange.', 'betterdocs' ) . '</li>'
368 . '</ol>';
369
370 return $this->p( __( 'Requesting a return is simple:', 'betterdocs' ) ) . $steps . $this->policy_line( $woo );
371 }
372
373 protected function answer_refund_timing( array $woo ) {
374 $first = __( 'Approved refunds are issued to your original payment method, typically within 5–10 business days after we receive the returned item.', 'betterdocs' );
375 $second = __( 'Return shipping is the customer\'s responsibility unless the item arrived damaged, defective, or incorrect — in which case we cover it.', 'betterdocs' );
376
377 return $this->p( $first ) . $this->p( $second ) . $this->policy_line( $woo );
378 }
379
380 protected function answer_place_order( array $woo = [] ) {
381 $shop = isset( $woo['shop_page_url'] ) ? (string) $woo['shop_page_url'] : '';
382 $first = '' !== $shop
383 ? sprintf(
384 /* translators: %s: link to the shop page. */
385 __( 'Browse %s, choose any options you need such as size or color, then click Add to Cart.', 'betterdocs' ),
386 '<a href="' . esc_url( $shop ) . '">' . esc_html__( 'our products', 'betterdocs' ) . '</a>'
387 )
388 : __( 'Browse our products and choose any options you need, such as size or color, then click Add to Cart.', 'betterdocs' );
389
390 $checkout = isset( $woo['checkout_page_url'] ) ? (string) $woo['checkout_page_url'] : '';
391 $second = '' !== $checkout
392 ? sprintf(
393 /* translators: %s: link to the checkout page. */
394 __( 'When you are ready, open your cart and proceed to %s, enter your shipping and payment details, and confirm your order. You will receive a confirmation email right away.', 'betterdocs' ),
395 '<a href="' . esc_url( $checkout ) . '">' . esc_html__( 'checkout', 'betterdocs' ) . '</a>'
396 )
397 : __( 'When you are ready, open your cart and select Checkout, enter your shipping and payment details, and confirm your order. You will receive a confirmation email right away.', 'betterdocs' );
398
399 return $this->p( $first ) . $this->p( $second );
400 }
401
402 protected function answer_modify_cancel() {
403 return $this->p( __( 'If you need to change or cancel an order, please contact us as soon as possible and we will do our best to update it before it ships.', 'betterdocs' ) )
404 . $this->p( __( 'Once an order has shipped it can no longer be changed, but you can still return it under our return policy.', 'betterdocs' ) );
405 }
406
407 protected function answer_account_status( array $woo = [] ) {
408 $account = isset( $woo['account_page_url'] ) ? (string) $woo['account_page_url'] : '';
409 $second = '' !== $account
410 ? sprintf(
411 /* translators: %s: link to the customer's account page. */
412 __( 'Creating an %s lets you track orders, save your addresses, and reorder faster. Either way, you will receive an email confirmation for every order and can reply to it any time with questions.', 'betterdocs' ),
413 '<a href="' . esc_url( $account ) . '">' . esc_html__( 'account', 'betterdocs' ) . '</a>'
414 )
415 : __( 'Creating an account lets you track orders, save your addresses, and reorder faster. Either way, you will receive an email confirmation for every order and can reply to it any time with questions.', 'betterdocs' );
416
417 return $this->p( __( 'An account is not required — you are welcome to check out as a guest.', 'betterdocs' ) ) . $this->p( $second );
418 }
419
420 /* --------------------------------------------------------------------- */
421 /* Helpers */
422 /* --------------------------------------------------------------------- */
423
424 /**
425 * A trailing "see our policies" line, linked to the Terms page when known.
426 *
427 * @return string
428 */
429 protected function policy_line( array $woo ) {
430 // Prefer the real refund/returns policy page; fall back to terms & conditions.
431 $url = isset( $woo['refund_policy_url'] ) ? (string) $woo['refund_policy_url'] : '';
432 $label = __( 'refund & returns policy', 'betterdocs' );
433
434 if ( '' === $url ) {
435 $url = isset( $woo['terms_page_url'] ) ? (string) $woo['terms_page_url'] : '';
436 $label = __( 'terms & policies', 'betterdocs' );
437 }
438
439 if ( '' === $url ) {
440 return '';
441 }
442
443 return $this->p(
444 sprintf(
445 /* translators: %s: link to the store's refund/returns or terms policy page. */
446 __( 'For full details, please see our %s.', 'betterdocs' ),
447 '<a href="' . esc_url( $url ) . '">' . esc_html( $label ) . '</a>'
448 )
449 );
450 }
451
452 /**
453 * Format a money amount using the store's currency symbol when known.
454 *
455 * @return string
456 */
457 protected function format_amount( $amount, array $woo ) {
458 $amount = (float) $amount;
459 // Trim a trailing ".00" for whole amounts, keep cents otherwise.
460 $number = ( floor( $amount ) === $amount ) ? number_format( $amount ) : number_format( $amount, 2 );
461 $symbol = isset( $woo['currency_symbol'] ) ? (string) $woo['currency_symbol'] : '';
462
463 if ( '' !== $symbol ) {
464 return $symbol . $number;
465 }
466
467 $code = isset( $woo['currency'] ) ? (string) $woo['currency'] : '';
468 return '' !== $code ? $number . ' ' . $code : $number;
469 }
470
471 /**
472 * Wrap text in a paragraph. Text is escaped; callers that embed a link build
473 * the HTML themselves and are wp_kses_post-sanitized downstream.
474 *
475 * @return string
476 */
477 protected function p( $text ) {
478 // Allow the few inline tags our answers use (links); escape the rest.
479 return '<p>' . wp_kses( $text, [ 'a' => [ 'href' => [], 'title' => [] ] ] ) . '</p>';
480 }
481
482 /**
483 * Natural-language list join: "A", "A and B", "A, B, and C".
484 *
485 * @return string
486 */
487 protected function human_list( array $items ) {
488 $items = array_values( array_filter( array_map( 'trim', array_map( 'strval', $items ) ) ) );
489 $count = count( $items );
490
491 if ( 0 === $count ) {
492 return '';
493 }
494 if ( 1 === $count ) {
495 return $items[0];
496 }
497 if ( 2 === $count ) {
498 /* translators: 1: first item, 2: second item. */
499 return sprintf( __( '%1$s and %2$s', 'betterdocs' ), $items[0], $items[1] );
500 }
501
502 $last = array_pop( $items );
503 /* translators: 1: comma-separated list of items, 2: final item. */
504 return sprintf( __( '%1$s, and %2$s', 'betterdocs' ), implode( ', ', $items ), $last );
505 }
506
507 /**
508 * Short plain-text excerpt from an answer's HTML.
509 *
510 * @return string
511 */
512 protected function excerpt( $html ) {
513 return wp_trim_words( wp_strip_all_tags( $html ), 22, '' );
514 }
515 }
516