PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.6.0
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.6.0
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.0, at includes/Core/StoreFaqContent.php

457 lines 18.6 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 /* Answer builders */
117 /* --------------------------------------------------------------------- */
118
119 /**
120 * @return string Safe HTML (paragraphs/lists/links) for one answer.
121 */
122 protected function answer_html( $id, array $woo, array $site ) {
123 switch ( $id ) {
124 case 'payment_methods':
125 return $this->answer_payment_methods( $woo );
126 case 'payment_security':
127 return $this->answer_payment_security( $site, $woo );
128 case 'currency_tax':
129 return $this->answer_currency_tax( $woo );
130 case 'shipping_coverage':
131 return $this->answer_shipping_coverage( $woo );
132 case 'shipping_cost':
133 return $this->answer_shipping_cost( $woo );
134 case 'order_tracking':
135 return $this->answer_order_tracking( $woo );
136 case 'return_policy':
137 return $this->answer_return_policy( $woo );
138 case 'return_how':
139 return $this->answer_return_how( $woo );
140 case 'refund_timing':
141 return $this->answer_refund_timing( $woo );
142 case 'place_order':
143 return $this->answer_place_order( $woo );
144 case 'modify_cancel':
145 return $this->answer_modify_cancel();
146 case 'account_status':
147 return $this->answer_account_status( $woo );
148 }
149
150 return '';
151 }
152
153 protected function answer_payment_methods( array $woo ) {
154 $methods = isset( $woo['payment_methods'] ) ? (array) $woo['payment_methods'] : [];
155
156 if ( ! empty( $methods ) ) {
157 $intro = sprintf(
158 /* translators: %s: list of payment method names. */
159 __( 'We accept %s.', 'betterdocs' ),
160 $this->human_list( $methods )
161 );
162 } else {
163 $intro = __( 'We accept all major payment methods, including popular credit and debit cards and digital wallets.', 'betterdocs' );
164 }
165
166 $second = __( 'You can review every available option on the checkout page before you place your order, and all payments are processed securely.', 'betterdocs' );
167
168 return $this->p( $intro ) . $this->p( $second );
169 }
170
171 protected function answer_payment_security( array $site, array $woo = [] ) {
172 $name = isset( $site['title'] ) && '' !== $site['title'] ? $site['title'] : __( 'our store', 'betterdocs' );
173
174 $first = __( 'Yes. Your payment is processed over a secure, encrypted (SSL) connection.', 'betterdocs' );
175 $second = sprintf(
176 /* translators: %s: site/store name. */
177 __( 'Card details are handled by our trusted payment provider — %s never stores your full card number on its own servers.', 'betterdocs' ),
178 $name
179 );
180
181 $privacy = isset( $woo['privacy_page_url'] ) ? (string) $woo['privacy_page_url'] : '';
182 $third = '';
183 if ( '' !== $privacy ) {
184 $third = $this->p(
185 sprintf(
186 /* translators: %s: link to the store's privacy policy page. */
187 __( 'See our %s for how your information is collected and used.', 'betterdocs' ),
188 '<a href="' . esc_url( $privacy ) . '">' . esc_html__( 'privacy policy', 'betterdocs' ) . '</a>'
189 )
190 );
191 }
192
193 return $this->p( $first ) . $this->p( $second ) . $third;
194 }
195
196 protected function answer_currency_tax( array $woo ) {
197 $currency = isset( $woo['currency'] ) ? (string) $woo['currency'] : '';
198 $symbol = isset( $woo['currency_symbol'] ) ? (string) $woo['currency_symbol'] : '';
199
200 if ( '' !== $currency ) {
201 $label = '' !== $symbol ? sprintf( '%s (%s)', $currency, $symbol ) : $currency;
202 $first = sprintf(
203 /* translators: %s: currency code (and symbol). */
204 __( 'All prices on our store are shown in %s.', 'betterdocs' ),
205 $label
206 );
207 } else {
208 $first = __( 'All prices are shown in our store currency, displayed throughout checkout.', 'betterdocs' );
209 }
210
211 if ( ! empty( $woo['tax_enabled'] ) ) {
212 $second = ! empty( $woo['prices_include_tax'] )
213 ? __( 'Applicable taxes are already included in the price you see.', 'betterdocs' )
214 : __( 'Any applicable taxes are calculated based on your location and shown at checkout before you pay.', 'betterdocs' );
215 } else {
216 $second = __( 'The total you see at checkout is the final amount you pay.', 'betterdocs' );
217 }
218
219 return $this->p( $first ) . $this->p( $second );
220 }
221
222 protected function answer_shipping_coverage( array $woo ) {
223 $regions = isset( $woo['shipping_regions'] ) ? (array) $woo['shipping_regions'] : [];
224 $location = isset( $woo['store_location'] ) ? (string) $woo['store_location'] : '';
225
226 if ( ! empty( $regions ) ) {
227 $first = sprintf(
228 /* translators: %s: list of shipping region names. */
229 __( 'We currently ship to %s.', 'betterdocs' ),
230 $this->human_list( $regions )
231 );
232 } else {
233 $first = __( 'We ship to all the destinations available at checkout — enter your address to see the options for your area.', 'betterdocs' );
234 }
235
236 $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' );
237
238 if ( '' !== $location ) {
239 $second .= ' ' . sprintf(
240 /* translators: %s: store base location. */
241 __( 'Orders ship from %s.', 'betterdocs' ),
242 $location
243 );
244 }
245
246 return $this->p( $first ) . $this->p( $second );
247 }
248
249 protected function answer_shipping_cost( array $woo = [] ) {
250 $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' );
251
252 $free_min = isset( $woo['free_shipping_min'] ) ? (float) $woo['free_shipping_min'] : 0;
253 if ( $free_min > 0 ) {
254 $first .= ' ' . sprintf(
255 /* translators: %s: formatted minimum order amount for free shipping. */
256 __( 'Orders over %s qualify for free shipping.', 'betterdocs' ),
257 $this->format_amount( $free_min, $woo )
258 );
259 }
260
261 return $this->p( $first )
262 . $this->p( __( 'Add your items to the cart and enter your address to view the available shipping rates for your order.', 'betterdocs' ) );
263 }
264
265 protected function answer_order_tracking( array $woo = [] ) {
266 $first = __( 'As soon as your order ships, we email you a confirmation with the details you need to follow its progress.', 'betterdocs' );
267 $account = isset( $woo['account_page_url'] ) ? (string) $woo['account_page_url'] : '';
268
269 if ( '' !== $account ) {
270 $second = sprintf(
271 /* translators: %s: link to the customer's account orders page. */
272 __( 'If you have an account, you can also track every order any time from your %s.', 'betterdocs' ),
273 '<a href="' . esc_url( $account ) . '">' . esc_html__( 'account\'s Orders page', 'betterdocs' ) . '</a>'
274 );
275 } else {
276 $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' );
277 }
278
279 return $this->p( $first ) . $this->p( $second );
280 }
281
282 protected function answer_return_policy( array $woo ) {
283 $window = isset( $woo['return_window'] ) ? (int) $woo['return_window'] : 0;
284
285 if ( $window > 0 ) {
286 $first = sprintf(
287 /* translators: %d: number of days in the return window. */
288 _n(
289 '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.',
290 '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.',
291 $window,
292 'betterdocs'
293 ),
294 $window
295 );
296 } else {
297 $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' );
298 }
299
300 return $this->p( $first ) . $this->policy_line( $woo );
301 }
302
303 protected function answer_return_how( array $woo = [] ) {
304 $steps = '<ol>'
305 . '<li>' . esc_html__( 'Contact us with your order number and the item you would like to return.', 'betterdocs' ) . '</li>'
306 . '<li>' . esc_html__( 'We confirm eligibility and send you return instructions.', 'betterdocs' ) . '</li>'
307 . '<li>' . esc_html__( 'Pack the item securely and ship it back to the address we provide.', 'betterdocs' ) . '</li>'
308 . '<li>' . esc_html__( 'Once it arrives, we process your refund or exchange.', 'betterdocs' ) . '</li>'
309 . '</ol>';
310
311 return $this->p( __( 'Requesting a return is simple:', 'betterdocs' ) ) . $steps . $this->policy_line( $woo );
312 }
313
314 protected function answer_refund_timing( array $woo ) {
315 $first = __( 'Approved refunds are issued to your original payment method, typically within 5–10 business days after we receive the returned item.', 'betterdocs' );
316 $second = __( 'Return shipping is the customer\'s responsibility unless the item arrived damaged, defective, or incorrect — in which case we cover it.', 'betterdocs' );
317
318 return $this->p( $first ) . $this->p( $second ) . $this->policy_line( $woo );
319 }
320
321 protected function answer_place_order( array $woo = [] ) {
322 $shop = isset( $woo['shop_page_url'] ) ? (string) $woo['shop_page_url'] : '';
323 $first = '' !== $shop
324 ? sprintf(
325 /* translators: %s: link to the shop page. */
326 __( 'Browse %s, choose any options you need such as size or color, then click Add to Cart.', 'betterdocs' ),
327 '<a href="' . esc_url( $shop ) . '">' . esc_html__( 'our products', 'betterdocs' ) . '</a>'
328 )
329 : __( 'Browse our products and choose any options you need, such as size or color, then click Add to Cart.', 'betterdocs' );
330
331 $checkout = isset( $woo['checkout_page_url'] ) ? (string) $woo['checkout_page_url'] : '';
332 $second = '' !== $checkout
333 ? sprintf(
334 /* translators: %s: link to the checkout page. */
335 __( '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' ),
336 '<a href="' . esc_url( $checkout ) . '">' . esc_html__( 'checkout', 'betterdocs' ) . '</a>'
337 )
338 : __( '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' );
339
340 return $this->p( $first ) . $this->p( $second );
341 }
342
343 protected function answer_modify_cancel() {
344 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' ) )
345 . $this->p( __( 'Once an order has shipped it can no longer be changed, but you can still return it under our return policy.', 'betterdocs' ) );
346 }
347
348 protected function answer_account_status( array $woo = [] ) {
349 $account = isset( $woo['account_page_url'] ) ? (string) $woo['account_page_url'] : '';
350 $second = '' !== $account
351 ? sprintf(
352 /* translators: %s: link to the customer's account page. */
353 __( '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' ),
354 '<a href="' . esc_url( $account ) . '">' . esc_html__( 'account', 'betterdocs' ) . '</a>'
355 )
356 : __( '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' );
357
358 return $this->p( __( 'An account is not required — you are welcome to check out as a guest.', 'betterdocs' ) ) . $this->p( $second );
359 }
360
361 /* --------------------------------------------------------------------- */
362 /* Helpers */
363 /* --------------------------------------------------------------------- */
364
365 /**
366 * A trailing "see our policies" line, linked to the Terms page when known.
367 *
368 * @return string
369 */
370 protected function policy_line( array $woo ) {
371 // Prefer the real refund/returns policy page; fall back to terms & conditions.
372 $url = isset( $woo['refund_policy_url'] ) ? (string) $woo['refund_policy_url'] : '';
373 $label = __( 'refund & returns policy', 'betterdocs' );
374
375 if ( '' === $url ) {
376 $url = isset( $woo['terms_page_url'] ) ? (string) $woo['terms_page_url'] : '';
377 $label = __( 'terms & policies', 'betterdocs' );
378 }
379
380 if ( '' === $url ) {
381 return '';
382 }
383
384 return $this->p(
385 sprintf(
386 /* translators: %s: link to the store's refund/returns or terms policy page. */
387 __( 'For full details, please see our %s.', 'betterdocs' ),
388 '<a href="' . esc_url( $url ) . '">' . esc_html( $label ) . '</a>'
389 )
390 );
391 }
392
393 /**
394 * Format a money amount using the store's currency symbol when known.
395 *
396 * @return string
397 */
398 protected function format_amount( $amount, array $woo ) {
399 $amount = (float) $amount;
400 // Trim a trailing ".00" for whole amounts, keep cents otherwise.
401 $number = ( floor( $amount ) === $amount ) ? number_format( $amount ) : number_format( $amount, 2 );
402 $symbol = isset( $woo['currency_symbol'] ) ? (string) $woo['currency_symbol'] : '';
403
404 if ( '' !== $symbol ) {
405 return $symbol . $number;
406 }
407
408 $code = isset( $woo['currency'] ) ? (string) $woo['currency'] : '';
409 return '' !== $code ? $number . ' ' . $code : $number;
410 }
411
412 /**
413 * Wrap text in a paragraph. Text is escaped; callers that embed a link build
414 * the HTML themselves and are wp_kses_post-sanitized downstream.
415 *
416 * @return string
417 */
418 protected function p( $text ) {
419 // Allow the few inline tags our answers use (links); escape the rest.
420 return '<p>' . wp_kses( $text, [ 'a' => [ 'href' => [], 'title' => [] ] ] ) . '</p>';
421 }
422
423 /**
424 * Natural-language list join: "A", "A and B", "A, B, and C".
425 *
426 * @return string
427 */
428 protected function human_list( array $items ) {
429 $items = array_values( array_filter( array_map( 'trim', array_map( 'strval', $items ) ) ) );
430 $count = count( $items );
431
432 if ( 0 === $count ) {
433 return '';
434 }
435 if ( 1 === $count ) {
436 return $items[0];
437 }
438 if ( 2 === $count ) {
439 /* translators: 1: first item, 2: second item. */
440 return sprintf( __( '%1$s and %2$s', 'betterdocs' ), $items[0], $items[1] );
441 }
442
443 $last = array_pop( $items );
444 /* translators: 1: comma-separated list of items, 2: final item. */
445 return sprintf( __( '%1$s, and %2$s', 'betterdocs' ), implode( ', ', $items ), $last );
446 }
447
448 /**
449 * Short plain-text excerpt from an answer's HTML.
450 *
451 * @return string
452 */
453 protected function excerpt( $html ) {
454 return wp_trim_words( wp_strip_all_tags( $html ), 22, '' );
455 }
456 }
457