array( 'on-bilgilendirme-formu', 'on-bilgilendirme', 'on-bilgilendirme-kosullari', 'pre-information-form' ), 'sales'=>array( 'mesafeli-satis-sozlesmesi', 'mesafeli-satis', 'distance-sales-contract' ), ); /** The seller details the documents need, with the length each may have. */ private const SELLER_FIELDS = array( 'title'=>200, 'mersis'=>32, 'tax_office'=>100, 'tax_number'=>20, 'address'=>400, 'phone'=>40, 'email'=>200, 'kep'=>200, 'complaint'=>400, 'return_carrier'=>100, 'return_open'=>1, 'return_code'=>60, 'return_address'=>400, 'dispatch_days'=>2, 'withdrawal_days'=>3, 'exceptions'=>1500, 'show_essentials'=>1, 'documents'=>9, 'merge_terms'=>1 ); private const KVKK = 'storesuit_checkout_kvkk_page'; /** The KVKK notice's usual addresses (the theme's footer finds the same page). */ private const KVKK_SLUGS = array( 'kvkk-aydinlatma-metni', 'kvkk', 'aydinlatma-metni', 'kisisel-verilerin-korunmasi', 'kvkk-aydinlatma' ); private const TITLES = array( 'preinfo'=>'Ön Bilgilendirme Formu', 'sales'=>'Mesafeli Satış Sözleşmesi' ); public static function hooks(): void { add_action( 'woocommerce_init', array( __CLASS__, 'register_field' ), 30 ); add_action( 'woocommerce_review_order_before_submit', array( __CLASS__, 'classic_box' ), 9 ); add_action( 'wp_enqueue_scripts', array( __CLASS__, 'assets' ), 20 ); add_action( 'woocommerce_checkout_process', array( __CLASS__, 'classic_check' ) ); add_action( 'woocommerce_checkout_create_order', array( __CLASS__, 'classic_record' ), 20 ); add_action( 'woocommerce_checkout_order_created', array( __CLASS__, 'classic_snapshot' ), 20 ); add_action( 'woocommerce_store_api_checkout_order_processed', array( __CLASS__, 'block_record' ), 20 ); add_filter( 'render_block_woocommerce/checkout', array( __CLASS__, 'block_links' ) ); add_action( 'add_meta_boxes', array( __CLASS__, 'meta_box' ), 30, 2 ); add_action( 'admin_post_storesuit_legal_evidence', array( __CLASS__, 'evidence_download' ) ); add_filter( 'woocommerce_order_actions', array( __CLASS__, 'order_actions' ), 20, 2 ); add_action( 'woocommerce_order_action_storesuit_send_legal', array( __CLASS__, 'send_documents' ) ); add_action( 'woocommerce_privacy_before_remove_order_personal_data', array( __CLASS__, 'erasing' ) ); add_action( 'save_post_page', static function (): void { delete_transient( self::CACHE ); delete_transient( self::KVKK ); } ); // One box for WooCommerce's terms and the contracts, and the privacy sentence pointing at the KVKK notice (Almira tour, 27 Oct 2026). add_filter( 'woocommerce_get_terms_and_conditions_checkbox_text', array( __CLASS__, 'terms_checkbox_text' ), 20 ); add_filter( 'woocommerce_get_privacy_policy_text', array( __CLASS__, 'privacy_text' ), 20, 2 ); add_action( 'wc_ajax_storesuit_legal_document', array( __CLASS__, 'ajax_document' ) ); add_filter( 'woocommerce_order_button_text', array( __CLASS__, 'button_text' ), 20 ); add_action( 'woocommerce_order_details_after_order_table', array( __CLASS__, 'customer_documents' ), 20 ); add_action( 'woocommerce_email_after_order_table', array( __CLASS__, 'email_documents' ), 30, 4 ); add_shortcode( 'storeruler_legal_document', array( __CLASS__, 'shortcode' ) ); } /** The pages the customer confirms, pre-information form first: array( key => array( id, title, url ) ). */ public static function pages(): array { static $memo = array(); $generated = self::generated(); // kept per state: saving the seller details changes the answer if ( isset( $memo[ (int) $generated ] ) ) { return $memo[ (int) $generated ]; } $ids = get_transient( self::CACHE ); if ( ! is_array( $ids ) ) { $ids = array(); foreach ( self::SLUGS as $key => $slugs ) { foreach ( $slugs as $slug ) { $page = get_page_by_path( $slug ); if ( $page instanceof \WP_Post && $page->post_status === 'publish' && $page->post_password === '' ) { $ids[ $key ] = (int) $page->ID; break; } } } set_transient( self::CACHE, $ids, HOUR_IN_SECONDS ); } $pages = array(); foreach ( $ids as $key => $id ) { if ( get_post_status( $id ) === 'publish' ) { $pages[ $key ] = array( 'id'=>(int) $id, 'title'=>html_entity_decode( get_the_title( $id ), ENT_QUOTES, 'UTF-8' ), 'url'=>(string) get_permalink( $id ), 'key'=>(string) $key ); } } // Generated documents stand in for missing pages, under the regulation's names; a filter may still add or replace pages. if ( $generated ) { foreach ( self::TITLES as $key => $title ) { if ( ! isset( $pages[ $key ] ) ) { $pages[ $key ] = array( 'id'=>0, 'title'=>$title, 'url'=>'', 'key'=>$key ); } } $pages = array_merge( array_intersect_key( $pages, self::TITLES ), $pages ); } return $memo[ (int) $generated ] = array_values( array_filter( (array) apply_filters( 'storeruler_checkout_legal_pages', $pages ), static fn( $page ) => is_array( $page ) && ! empty( $page['title'] ) && ( ! empty( $page['url'] ) || isset( self::TITLES[ $page['key'] ?? '' ] ) ) ) ); } /* ---------- Seller details ---------- */ /** A store selling from Turkey: the documents follow Turkish distance-sales law. */ public static function turkish(): bool { return function_exists( 'WC' ) && WC()->countries && WC()->countries->get_base_country() === 'TR'; } /** The seller details as saved, each key present. */ public static function seller(): array { $saved = get_option( self::SELLER, array() ); $saved = is_array( $saved ) ? $saved : array(); $out = array(); foreach ( array_keys( self::SELLER_FIELDS ) as $key ) { $out[ $key ] = trim( (string) ( $saved[ $key ] ?? '' ) ); } $out['dispatch_days'] = $out['dispatch_days'] === '' ? '' : (string) max( 1, min( 30, (int) $out['dispatch_days'] ) ); // The merchant's choices, on by default (owner, 27 Oct 2026): the essentials above the order button (m.6/2 a) and // the documents written by StoreRuler rather than the store's own pages. $out['show_essentials'] = array_key_exists( 'show_essentials', $saved ) && (string) $saved['show_essentials'] === '0' ? '0' : '1'; // WooCommerce's terms box joins the contracts box unless the merchant keeps it apart (27 Oct 2026). $out['merge_terms'] = array_key_exists( 'merge_terms', $saved ) && (string) $saved['merge_terms'] === '0' ? '0' : '1'; $out['documents'] = $out['documents'] === 'pages' ? 'pages' : 'generated'; return $out; } /** What the settings form starts from: the saved details, else the store's own name, address, phone and e-mail. */ public static function seller_form(): array { $seller = self::seller(); $saved = get_option( self::SELLER, false ) !== false; if ( ! $saved ) { $countries = WC()->countries; $states = $countries->get_states( $countries->get_base_country() ); $state = $countries->get_base_state(); $region = is_array( $states ) && isset( $states[ $state ] ) ? $states[ $state ] : $state; $seller['title'] = html_entity_decode( (string) get_bloginfo( 'name' ), ENT_QUOTES, 'UTF-8' ); $seller['address'] = implode( ' ', array_filter( array( $countries->get_base_address(), $countries->get_base_address_2(), trim( $countries->get_base_postcode() . ' ' . $countries->get_base_city() ), $region ) ) ); $seller['email'] = (string) get_option( 'woocommerce_email_from_address', get_option( 'admin_email' ) ); $seller['phone'] = (string) get_theme_mod( 'sr_text_contact_phone', '' ); $seller['dispatch_days'] = '3'; $seller['withdrawal_days'] = '14'; } return array( 'values'=>$seller, 'saved'=>$saved, 'configured'=>self::configured(), 'turkish'=>self::turkish(), 'missing'=>self::missing(), 'disclaimer'=>self::disclaimer(), 'versions'=>array( 'preinfo'=>self::template_version( 'preinfo' ), 'sales'=>self::template_version( 'sales' ) ) ); } /** The details the regulation requires that are still empty (m.5/1 b, c, ğ and g). */ public static function missing(): array { $s = self::seller(); $missing = array(); if ( $s['title'] === '' ) { $missing[] = 'title'; } if ( $s['mersis'] === '' && $s['tax_number'] === '' ) { $missing[] = 'mersis'; } foreach ( array( 'address', 'phone', 'email', 'dispatch_days' ) as $key ) { if ( $s[ $key ] === '' ) { $missing[] = $key; } } // A named carrier for returns, or the seller's choice to name none (then returns cost the buyer nothing, m.12/5). if ( $s['return_carrier'] === '' && $s['return_open'] !== '1' ) { $missing[] = 'return_carrier'; } return $missing; } public static function configured(): bool { return ! self::missing(); } /** Documents written here instead of the pages: a Turkish store with complete seller details. */ public static function generated(): bool { return self::turkish() && self::wants_generated() && self::configured(); } /** The merchant chose StoreRuler's written documents (the default) rather than their own published pages. */ public static function wants_generated(): bool { return self::seller()['documents'] !== 'pages'; } public static function seller_save( array $data ): array { Store::require_cap( 'manage_woocommerce' ); $out = array(); foreach ( self::SELLER_FIELDS as $key => $limit ) { $raw = isset( $data[ $key ] ) && is_scalar( $data[ $key ] ) ? (string) $data[ $key ] : ''; // JSON from the admin screen (Admin::ajax()), not slashed. $value = trim( in_array( $key, array( 'address', 'complaint', 'return_address', 'exceptions' ), true ) ? sanitize_textarea_field( $raw ) : sanitize_text_field( $raw ) ); if ( mb_strlen( $value ) > $limit ) { throw new \InvalidArgumentException( I18n::t( 'One of the seller details is too long.' ) ); } $out[ $key ] = $value; } if ( $out['email'] !== '' && ! is_email( $out['email'] ) ) { throw new \InvalidArgumentException( I18n::t( 'Enter a valid e-mail address for the seller.' ) ); } if ( $out['mersis'] !== '' && ! preg_match( '/^\d{16}$/', preg_replace( '/\s+/', '', $out['mersis'] ) ) ) { throw new \InvalidArgumentException( I18n::t( 'A MERSİS number has 16 digits.' ) ); } if ( $out['tax_number'] !== '' && ! preg_match( '/^\d{10,11}$/', $out['tax_number'] ) ) { throw new \InvalidArgumentException( I18n::t( 'A tax number has 10 digits (11 for a sole trader\'s identity number).' ) ); } if ( $out['dispatch_days'] !== '' && ( ! ctype_digit( $out['dispatch_days'] ) || (int) $out['dispatch_days'] < 1 || (int) $out['dispatch_days'] > 30 ) ) { throw new \InvalidArgumentException( I18n::t( 'The dispatch time is 1 to 30 days: the law allows at most 30 days.' ) ); } if ( $out['withdrawal_days'] !== '' && ( ! ctype_digit( $out['withdrawal_days'] ) || (int) $out['withdrawal_days'] < 14 || (int) $out['withdrawal_days'] > 365 ) ) { throw new \InvalidArgumentException( I18n::t( 'The return period is at least 14 days, the legal minimum (at most 365).' ) ); } $out['mersis'] = preg_replace( '/\s+/', '', $out['mersis'] ); $out['show_essentials'] = in_array( $out['show_essentials'], array( '0', 'false', 'off', 'no' ), true ) ? '0' : '1'; $out['merge_terms'] = in_array( $out['merge_terms'], array( '0', 'false', 'off', 'no' ), true ) ? '0' : '1'; $out['documents'] = $out['documents'] === 'pages' ? 'pages' : 'generated'; $out['return_open'] = in_array( $out['return_open'], array( '1', 'true', 'on', 'yes' ), true ) ? '1' : ''; // The texts are drafts and the merchant's to check: confirmed once, before the written contracts are first turned on. $was = self::generated(); $previous = get_option( self::SELLER, false ); update_option( self::SELLER, $out, false ); if ( ! $was && self::generated() && ! self::disclaimer() ) { if ( empty( $data['disclaimer'] ) ) { if ( $previous === false ) { delete_option( self::SELLER ); } else { update_option( self::SELLER, $previous, false ); } throw new \InvalidArgumentException( I18n::t( 'Confirm that you have read the notice about the contract drafts first.' ) ); } self::disclaimer_accept(); } delete_transient( self::CACHE ); Store::audit( 'settings', 0, 'legal_seller_saved', array( 'fields'=>array_keys( array_filter( $out, static fn( $v ) => $v !== '' ) ) ) ); return self::seller_form(); } /* ---------- Checkout ---------- */ /** "I have read and accept the …": the documents' titles, or (with $links) buttons that open them; escaped either way. */ private static function sentence( bool $links ): string { $pages = self::pages(); $terms = $pages ? self::merged_terms() : null; if ( $terms ) { $pages[] = $terms; } $names = array_map( static fn( array $page ): string => $links ? self::opener( $page ) : $page['title'], $pages ); if ( count( $names ) > 2 ) { return sprintf( $links ? esc_html( I18n::t( 'I have read and accept the %1$s, the %2$s and the %3$s.' ) ) : I18n::t( 'I have read and accept the %1$s, the %2$s and the %3$s.' ), $names[0], $names[1], $names[2] ); } if ( count( $names ) > 1 ) { return sprintf( $links ? esc_html( I18n::t( 'I have read and accept the %1$s and the %2$s.' ) ) : I18n::t( 'I have read and accept the %1$s and the %2$s.' ), $names[0], $names[1] ); } return $names ? sprintf( $links ? esc_html( I18n::t( 'I have read and accept the %s.' ) ) : I18n::t( 'I have read and accept the %s.' ), $names[0] ) : ''; } /** * WooCommerce's terms and conditions page when its own box joins the contracts box (the merchant's choice, on by * default): published, not one of the contracts, and only while the contracts box is shown. Otherwise null. * @return array{id:int,title:string,url:string,key:string}|null */ public static function merged_terms(): ?array { if ( self::seller()['merge_terms'] !== '1' || ! function_exists( 'wc_terms_and_conditions_page_id' ) || ! self::pages() ) { return null; } $id = (int) wc_terms_and_conditions_page_id(); if ( ! $id || get_post_status( $id ) !== 'publish' || in_array( $id, array_map( 'intval', wp_list_pluck( self::pages(), 'id' ) ), true ) ) { return null; } return array( 'id'=>$id, 'title'=>html_entity_decode( get_the_title( $id ), ENT_QUOTES, 'UTF-8' ), 'url'=>(string) get_permalink( $id ), 'key'=>'terms' ); } /** WooCommerce's terms box (its text) disappears when the terms page is part of the contracts box; WooCommerce then checks nothing of its own. */ public static function terms_checkbox_text( $text ) { return self::merged_terms() ? '' : $text; } /** The published KVKK notice of a Turkish store, or null (found by its usual addresses, kept until a page is saved). */ public static function kvkk_page(): ?array { $id = get_transient( self::KVKK ); if ( $id === false ) { $id = 0; foreach ( self::KVKK_SLUGS as $slug ) { $page = get_page_by_path( $slug ); if ( $page instanceof \WP_Post && $page->post_status === 'publish' && $page->post_password === '' ) { $id = (int) $page->ID; break; } } set_transient( self::KVKK, $id, DAY_IN_SECONDS ); } $id = (int) $id; return $id && get_post_status( $id ) === 'publish' ? array( 'id'=>$id, 'title'=>html_entity_decode( get_the_title( $id ), ENT_QUOTES, 'UTF-8' ), 'url'=>(string) get_permalink( $id ) ) : null; } /** WooCommerce's privacy sentence at checkout and sign-up: in a Turkish store with a KVKK notice, its link is that notice. */ public static function privacy_text( $text, $type = '' ) { if ( ! is_string( $text ) || ! str_contains( $text, '[privacy_policy]' ) || ! self::turkish() ) { return $text; } $page = self::kvkk_page(); if ( ! $page ) { return $text; } return str_replace( '[privacy_policy]', '' . esc_html( $page['title'] ) . '', $text ); } /** A link that opens the document in the window (assets/checkout-legal.js); without the script, the page in a new tab. */ private static function opener( array $page ): string { $key = (string) ( $page['key'] ?? '' ); return '' . esc_html( $page['title'] ) . ''; } /** * woocommerce_init: the block checkout's box. A shop page shown to a shopper (a GET for HTML) has no use for it, and * reading the documents and their sentence there cost every product and category page about 7 ms (Almira, 27 Oct * 2026): such a request registers it once its page is known, and only on a checkout. The Store API, admin, AJAX, * cron and posted forms register it at once, as before. */ public static function register_field(): void { if ( ! function_exists( 'woocommerce_register_additional_checkout_field' ) ) { return; } if ( self::shopper_page_request() ) { add_action( 'wp', array( __CLASS__, 'register_on_checkout' ), 0 ); return; } self::register_now(); } /** wp: a shopper's page that draws a checkout (the WooCommerce checkout, or any page with the checkout block). */ public static function register_on_checkout(): void { $post = get_queried_object(); if ( ( function_exists( 'is_checkout' ) && is_checkout() ) || ( $post instanceof \WP_Post && has_block( 'woocommerce/checkout', $post ) ) ) { self::register_now(); } } /** A plain front-end GET for a page: not wp-admin, AJAX, cron, the REST or Store API, a posted form or WP-CLI. */ private static function shopper_page_request(): bool { if ( is_admin() || wp_doing_ajax() || wp_doing_cron() || ( defined( 'WP_CLI' ) && WP_CLI ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) { return false; } if ( ( $_SERVER['REQUEST_METHOD'] ?? 'GET' ) !== 'GET' || isset( $_GET['rest_route'] ) || isset( $_GET['wc-ajax'] ) ) { return false; } // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput -- only the kind of request is read and compared with a fixed word. $path = (string) wp_parse_url( (string) ( $_SERVER['REQUEST_URI'] ?? '' ), PHP_URL_PATH ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- compared, never printed. return ! str_contains( $path, '/' . rest_get_url_prefix() . '/' ); } private static function register_now(): void { if ( ! self::pages() ) { return; } try { woocommerce_register_additional_checkout_field( array( 'id'=>self::FIELD, 'label'=>self::sentence( false ), 'location'=>'order', 'type'=>'checkbox', 'required'=>true, 'show_in_order_confirmation'=>false ) ); } catch ( \Throwable $e ) { /* An older WooCommerce: the classic checkout keeps its own box. */ } } public static function classic_box(): void { if ( ! self::pages() ) { return; } $checked = ! empty( $_POST['storesuit_legal_consent'] ); // phpcs:ignore WordPress.Security.NonceVerification -- redisplay after a refused order; WooCommerce checks its own nonce. echo self::essentials(); // phpcs:ignore WordPress.Security.EscapeOutput -- escaped in essentials(). echo '
'; // phpcs:ignore WordPress.Security.EscapeOutput -- escaped in sentence(). } /** * Right above the order button, as one block (m.6/2 a, b): the goods, the total with taxes and delivery, the withdrawal * right with the carrier for returns, when it does not apply, the payment methods and where the store delivers. Part of * the order summary WooCommerce redraws, so the figures follow the cart. Turkish stores with written documents, unless * the merchant turned it off (a choice since 27 Oct 2026; on by default, as the regulation asks). */ private static function essentials(): string { if ( ! self::generated() || self::seller()['show_essentials'] !== '1' || ! function_exists( 'WC' ) || ! WC()->cart ) { return ''; } $s = self::seller(); $cart = WC()->cart; $goods = array(); $days = max( 14, (int) ( $s['withdrawal_days'] ?: 14 ) ); foreach ( $cart->get_cart() as $item ) { $product = $item['data'] ?? null; if ( ! $product instanceof \WC_Product ) { continue; } $goods[] = Store::plain( $product->get_name() ) . ' × ' . wc_stock_amount( $item['quantity'] ); } $more = count( $goods ) > 3 ? ' +' . ( count( $goods ) - 3 ) : ''; $gateways = array(); foreach ( WC()->payment_gateways()->get_available_payment_gateways() as $gateway ) { $gateways[] = wp_strip_all_tags( $gateway->get_title() ); } $countries = WC()->countries->get_shipping_countries(); $where = count( $countries ) === 1 ? reset( $countries ) . ' içi' : ( $countries ? count( $countries ) . ' ülke' : '' ); $rows = array( array( 'Ürünler', esc_html( implode( ', ', array_slice( $goods, 0, 3 ) ) . $more ) ), array( 'Toplam', wp_kses_post( wc_price( (float) $cart->get_total( 'edit' ) ) ) . ' (tüm vergiler ve teslimat dahil)' ), array( 'Cayma hakkı', esc_html( 'Teslimden itibaren ' . $days . ' gün içinde gerekçe göstermeden cayabilirsiniz. ' . ( $s['return_open'] === '1' ? 'İade ücretsizdir; kargo kodunu satıcı bildirir.' : 'İade: ' . $s['return_carrier'] . ', ücretsiz.' ) ) ), array( 'İstisnalar', esc_html( 'Ambalajı açılmış hijyen ürünleri, kişiye özel hazırlanan ve çabuk bozulan ürünlerde cayma hakkı yoktur (tam liste formda).' ) ), array( 'Teslimat', esc_html( 'En geç ' . $s['dispatch_days'] . ' iş gününde kargoda' . ( $where !== '' ? ' · Gönderim: ' . $where : '' ) ) ), ); if ( $gateways ) { $rows[] = array( 'Ödeme', esc_html( implode( ', ', $gateways ) ) ); } $html = 'Siparişinizin temel bilgileri
' . sprintf( esc_html( I18n::t( 'Read before you order: %s' ) ), $links ) . '
'; } /** * wc-ajax=storesuit_legal_document: one document for the shopper's own cart, with what the checkout form holds now (the * classic form posts its fields; the block checkout's address is already in the customer session). Reads only. */ public static function ajax_document(): void { $type = sanitize_key( (string) ( $_POST['document'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification -- reads the visitor's own cart; nothing is changed. $page = absint( $_POST['page'] ?? 0 ); // phpcs:ignore WordPress.Security.NonceVerification if ( isset( self::TITLES[ $type ] ) && self::generated() ) { $posted = array(); foreach ( (array) wp_unslash( $_POST ) as $key => $value ) { if ( is_string( $key ) && is_scalar( $value ) && preg_match( '/^(billing|shipping)_[a-z0-9_]+$|^(payment_method|ship_to_different_address)$/', $key ) ) { $posted[ $key ] = sanitize_text_field( (string) $value ); } } // phpcs:ignore WordPress.Security.NonceVerification wp_send_json_success( array( 'title'=>self::TITLES[ $type ], 'html'=>self::document( $type, self::cart_context( $posted ) ) ) ); } // A published page (no seller details, or another country): its own content. $ids = wp_list_pluck( self::pages(), 'id' ); $terms = self::merged_terms(); if ( $terms ) { $ids[] = $terms['id']; } if ( $page && in_array( $page, array_map( 'intval', $ids ), true ) ) { $post = get_post( $page ); wp_send_json_success( array( 'title'=>html_entity_decode( get_the_title( $post ), ENT_QUOTES, 'UTF-8' ), 'html'=>apply_filters( 'the_content', $post->post_content ) ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- WordPress's own filter. } wp_send_json_error( array( 'message'=>I18n::t( 'The document could not be opened. Open it on its own page.' ) ), 404 ); } /** [storeruler_legal_document type="preinfo|sales"]: the store's document on its page; the buyer's parts say when they are filled. */ public static function shortcode( $atts ): string { $type = sanitize_key( (string) ( is_array( $atts ) ? ( $atts['type'] ?? 'preinfo' ) : 'preinfo' ) ); if ( ! isset( self::TITLES[ $type ] ) || ! self::turkish() ) { return ''; } if ( ! self::configured() ) { return current_user_can( 'manage_woocommerce' ) ? '' . esc_html( I18n::t( 'Complete the seller and contract details in StoreRuler › Settings to show this document.' ) ) . '
' : ''; } return '' . esc_html( I18n::t( 'Download the evidence file' ) ) . '
' . self::kept( $order ); } return $html; } /** * The evidence file: the record and each document exactly as kept, between and * ; the bytes between the two markers hash to the SHA-256 shown above them. */ public static function evidence( \WC_Order $order ): string { $kept = $order->get_meta( self::DOCUMENTS ); $kept = is_array( $kept ) ? $kept : array(); $html = '' . esc_html( html_entity_decode( (string) get_bloginfo( 'name' ), ENT_QUOTES, 'UTF-8' ) ) . ' · ' . esc_url( home_url( '/' ) ) . ' · ' . esc_html( I18n::format( 'file made %s', gmdate( 'Y-m-d H:i:s' ) . ' UTC' ) ) . '
'; $html .= self::record_html( $order, false ); $html .= '' . esc_html( I18n::t( 'Each document below is the text between its begin and end markers in this file; its SHA-256 equals the value shown with it.' ) ) . '
'; foreach ( (array) ( $kept['documents'] ?? array() ) as $type => $document ) { if ( ! is_string( $document ) ) { continue; } $key = sanitize_key( (string) $type ); $html .= 'SHA-256 ' . esc_html( (string) ( $kept['hashes'][ $type ] ?? hash( 'sha256', $document ) ) ) . '
Ürünler, siparişin satıcıya ulaşmasından (havale/EFT ile ödemelerde ödemenin satıcının hesabına geçmesinden) itibaren en geç {dispatch_days} iş günü içinde kargoya verilir; teslim süresi her halükarda siparişin satıcıya ulaşmasından itibaren 30 günü geçemez. Teslimat, alıcının bildirdiği teslimat adresinde alıcıya veya adreste bulunan kişiye yapılır.
' . 'Satıcı, mal alıcıya veya alıcının taşıyıcı dışında belirleyeceği üçüncü bir kişiye teslim edilinceye kadar oluşan kayıp ve hasarlardan sorumludur. Alıcının, satıcının belirlediği taşıyıcı dışında başka bir taşıyıcı ile gönderim istemesi halinde, malın o taşıyıcıya tesliminden itibaren oluşan kayıp ve hasardan satıcı sorumlu değildir. Alıcının, teslim sırasında hasarlı, ezik veya açılmış paketleri kargo görevlisine tutanak tutturarak teslim alması ya da teslim almaması, hakkını kolay ispatlaması için önemlidir.
' . 'Siparişin ifası imkânsızlaşırsa satıcı bunu öğrendiği tarihten itibaren 3 gün içinde alıcıya yazılı olarak veya kalıcı veri saklayıcısı ile bildirir ve varsa teslimat masrafları dahil tahsil ettiği tüm ödemeleri bildirim tarihinden itibaren en geç 14 gün içinde iade eder. Satıcı yükümlülüğünü taahhüt ettiği sürede yerine getirmezse alıcı sözleşmeyi feshedebilir.
'; $withdrawal = 'Alıcı, hiçbir gerekçe göstermeksizin ve cezai şart ödemeksizin, malı teslim aldığı (birden fazla parçadan oluşan veya ayrı ayrı teslim edilen siparişlerde son malı/parçayı teslim aldığı) günden itibaren {return_days} içinde sözleşmeden cayma hakkına sahiptir{return_days_note}. Cayma hakkı, sözleşmenin kurulmasından malın teslimine kadar olan süre içinde de kullanılabilir. Malın satıcı tarafından taşıyıcıya teslimi, alıcıya teslim sayılmaz.
' . '{withdrawal_notice} Bu formun sonundaki örnek cayma formu kullanılabileceği gibi cayma kararını bildiren açık bir beyan da yeterlidir. Cayma hakkının süresi içinde kullanıldığını ispat yükü alıcıya aittir.
' . '{return_shipping}' . 'Satıcı; malın iade için {refund_start} tarihten itibaren 14 gün içinde, varsa teslimat masrafları da dahil olmak üzere tahsil ettiği tüm ödemeleri, alıcının kullandığı ödeme aracına uygun şekilde, masrafsız ve tek seferde iade eder. Malın tesliminden önce cayılırsa bu süre bildirimin satıcıya ulaştığı tarihten başlar. Kartla yapılan ödemelerde iadenin karta yansıma süresi kartı çıkaran bankaya bağlıdır.
' . 'Alıcı, cayma süresi içinde malı işleyişine, teknik özelliklerine ve kullanım talimatlarına uygun kullandığı takdirde meydana gelen değişiklik ve bozulmalardan sorumlu değildir; bu kullanımın dışında oluşan değişiklik, bozulma ve eksikliklerden doğan değer kaybı için satıcının kanuni hakları saklıdır. Malın faturası (varsa), aksesuarları, hediye ve promosyon ürünleri ile birlikte, mümkünse orijinal ambalajı ve etiketiyle gönderilmesi rica olunur; kampanya kapsamında hediye olarak verilen ürünler cayma halinde malla birlikte iade edilir.
'; $complaints = 'Talep ve şikâyetler satıcıya {complaint_contacts}. Satıcı şikâyetleri en kısa sürede yanıtlar.
' . 'Uyuşmazlıklarda alıcı, Ticaret Bakanlığınca her yıl ilan edilen parasal sınırlar dahilinde, mal veya hizmeti satın aldığı veya yerleşim yerinin bulunduğu yerdeki Tüketici Hakem Heyetine; bu sınırları aşan uyuşmazlıklarda 6502 sayılı Kanunun 73/A maddesi uyarınca dava açılmadan önce arabulucuya başvurulması şartıyla Tüketici Mahkemesine başvurabilir. Başvurular e-Devlet üzerinden Tüketici Bilgi Sistemi (TÜBİS) ile de yapılabilir.
'; $data = 'Alıcının kişisel verileri, siparişin kurulması ve ifası, faturalandırma, teslimat ve yasal yükümlülükler için 6698 sayılı Kişisel Verilerin Korunması Kanunu kapsamında işlenir{privacy_link}. Satıcı, bu sözleşmeye ilişkin işlem kayıtlarını 3 yıl süreyle saklar.
'; if ( $type === 'preinfo' ) { return '6502 sayılı Tüketicinin Korunması Hakkında Kanun ve Mesafeli Sözleşmeler Yönetmeliği uyarınca, alıcı sipariş vermeden ve ödeme yükümlülüğü altına girmeden önce aşağıdaki konularda bilgilendirilir. Bu form mesafeli satış sözleşmesinin ayrılmaz bir parçasıdır.
' . 'Fiyatlara tüm vergiler dahildir. Bu formda belirtilenler dışında alıcıdan ek bir masraf talep edilmez; alıcının açık onayı olmadan ek ödeme doğuran bir seçenek önceden seçili sunulmaz. Kartla taksitli ödemelerde taksit koşulları ödeme adımında gösterilir.
' . 'Alıcı, sipariş vermeden önce bu formdaki bilgileri okuduğunu, siparişini onaylamasının ödeme yükümlülüğü doğurduğunu bildiğini ve bu bilgileri elektronik ortamda teyit ettiğini kabul eder. Form ve sözleşme sipariş onayıyla birlikte alıcının e-posta adresine gönderilir.
{sample_form}'; } return 'Satıcı
{seller_table}Alıcı
{buyer_table}' . 'Bu sözleşmenin konusu, alıcının satıcıya ait {site_url} internet sitesinden elektronik ortamda siparişini verdiği, aşağıda nitelikleri ve satış fiyatı belirtilen ürünlerin satışı ve teslimine ilişkin olarak, 6502 sayılı Tüketicinin Korunması Hakkında Kanun ve Mesafeli Sözleşmeler Yönetmeliği hükümleri gereğince tarafların hak ve yükümlülüklerinin belirlenmesidir.
' . 'Fatura, alıcının sipariş sırasında bildirdiği fatura bilgilerine düzenlenir ve ürünle birlikte veya elektronik ortamda iletilir.
' . 'Alıcı, siparişi onaylayarak bu sözleşmenin tüm koşullarını kabul etmiş sayılır. Sözleşme, siparişin onaylandığı anda kurulur ve yürürlüğe girer; bir örneği alıcının e-posta adresine gönderilir ve hesabındaki sipariş sayfasında saklanır.
{signature}'; } /** The template in use: the merchant's saved text, else StoreRuler's. */ public static function template( string $type ): string { $saved = get_option( self::TEMPLATES, array() ); $html = is_array( $saved ) ? (string) ( $saved[ $type ]['html'] ?? '' ) : ''; return $html !== '' ? $html : self::default_template( $type ); } /** The version number of a document's template: 0 for StoreRuler's text never edited, then one per save. */ public static function template_version( string $type ): int { $saved = get_option( self::TEMPLATES, array() ); return is_array( $saved ) ? (int) ( $saved[ $type ]['version'] ?? 0 ) : 0; } /** Placeholders the template uses that do not exist, and required sections it no longer has: warnings only. */ public static function template_warnings( string $html ): array { preg_match_all( '/\{([a-z_]+)\}/', $html, $found ); $warnings = array(); foreach ( array_unique( $found[1] ) as $key ) { if ( ! isset( self::PLACEHOLDERS[ $key ] ) ) { $warnings[] = I18n::format( 'Unknown placeholder {%s}: it will be shown as written.', $key ); } } foreach ( self::REQUIRED as $key ) { if ( ! str_contains( $html, '{' . $key . '}' ) ) { $warnings[] = I18n::format( 'The required part {%1$s} is missing: %2$s.', $key, I18n::t( self::PLACEHOLDERS[ $key ] ) ); } } return $warnings; } /** What the template editor reads: the text, StoreRuler's text, the placeholders, the version history, the notice state. */ public static function template_read( array $data ): array { Store::require_cap( 'manage_woocommerce' ); $type = sanitize_key( (string) ( $data['type'] ?? '' ) ); if ( ! isset( self::TITLES[ $type ] ) ) { throw new \InvalidArgumentException( I18n::t( 'Choose the pre-information form or the distance sales contract.' ) ); } $history = array_values( array_filter( (array) get_option( self::HISTORY, array() ), static fn( $h ) => is_array( $h ) && ( $h['type'] ?? '' ) === $type ) ); $placeholders = array(); foreach ( self::PLACEHOLDERS as $key => $about ) { $placeholders[] = array( 'key'=>'{' . $key . '}', 'about'=>I18n::t( $about ), 'required'=>in_array( $key, self::REQUIRED, true ) ); } $html = self::template( $type ); return array( 'type'=>$type, 'title'=>self::TITLES[ $type ], 'html'=>$html, 'custom'=>$html !== self::default_template( $type ), 'version'=>self::template_version( $type ), 'placeholders'=>$placeholders, 'warnings'=>self::template_warnings( $html ), 'history'=>array_map( static fn( $h ) => array( 'version'=>(int) $h['version'], 'time'=>wp_date( 'Y-m-d H:i', (int) $h['time'] ), 'by'=>( $u = get_userdata( (int) $h['user'] ) ) ? $u->display_name : '#' . (int) $h['user'], 'default'=>! empty( $h['default'] ), 'hash'=>substr( (string) $h['hash'], 0, 12 ) ), array_reverse( $history ) ), 'disclaimer'=>self::disclaimer() ); } /** Saves a document's text (or StoreRuler's again with $data['restore']), as a new version; old orders keep what they accepted. */ public static function template_save( array $data ): array { Store::require_cap( 'manage_woocommerce' ); $type = sanitize_key( (string) ( $data['type'] ?? '' ) ); if ( ! isset( self::TITLES[ $type ] ) ) { throw new \InvalidArgumentException( I18n::t( 'Choose the pre-information form or the distance sales contract.' ) ); } if ( ! self::disclaimer() && empty( $data['disclaimer'] ) ) { throw new \InvalidArgumentException( I18n::t( 'Confirm that you have read the notice about the contract drafts first.' ) ); } if ( ! self::disclaimer() ) { self::disclaimer_accept(); } $restore = ! empty( $data['restore'] ); $html = $restore ? '' : trim( wp_kses_post( isset( $data['html'] ) && is_scalar( $data['html'] ) ? (string) $data['html'] : '' ) ); if ( ! $restore && $html === '' ) { throw new \InvalidArgumentException( I18n::t( 'The text is empty. Restore StoreRuler\'s text instead.' ) ); } if ( mb_strlen( $html ) > 200000 ) { throw new \InvalidArgumentException( I18n::t( 'The text is too long.' ) ); } if ( $html === self::default_template( $type ) ) { $html = ''; } // the same as StoreRuler's: kept as "default" $saved = get_option( self::TEMPLATES, array() ); $saved = is_array( $saved ) ? $saved : array(); $version = self::template_version( $type ) + 1; $time = time(); $saved[ $type ] = array( 'html'=>$html, 'version'=>$version, 'time'=>$time, 'user'=>get_current_user_id() ); update_option( self::TEMPLATES, $saved, false ); $history = (array) get_option( self::HISTORY, array() ); $history[] = array( 'type'=>$type, 'version'=>$version, 'time'=>$time, 'user'=>get_current_user_id(), 'default'=>$html === '', 'hash'=>hash( 'sha256', $html === '' ? self::default_template( $type ) : $html ), 'html'=>$html ); $mine = array_keys( array_filter( $history, static fn( $h ) => is_array( $h ) && ( $h['type'] ?? '' ) === $type ) ); foreach ( array_slice( $mine, 0, max( 0, count( $mine ) - 30 ) ) as $old ) { unset( $history[ $old ] ); } // the last 30 versions of each document update_option( self::HISTORY, array_values( $history ), false ); Store::audit( 'settings', 0, 'legal_template_saved', array( 'type'=>$type, 'version'=>$version, 'default'=>$html === '' ) ); return self::template_read( array( 'type'=>$type ) ); } /** The merchant's confirmation that the texts are drafts and theirs to check: who and when, or null. */ public static function disclaimer(): ?array { $d = get_option( self::DISCLAIMER, null ); return is_array( $d ) && ! empty( $d['user'] ) ? array( 'user'=>(int) $d['user'], 'time'=>(int) $d['time'], 'by'=>( $u = get_userdata( (int) $d['user'] ) ) ? $u->display_name : '#' . (int) $d['user'], 'at'=>wp_date( 'Y-m-d H:i', (int) $d['time'] ) ) : null; } public static function disclaimer_accept(): array { Store::require_cap( 'manage_woocommerce' ); if ( ! self::disclaimer() ) { update_option( self::DISCLAIMER, array( 'user'=>get_current_user_id(), 'time'=>time() ), false ); Store::audit( 'settings', 0, 'legal_disclaimer_accepted', array() ); } return self::seller_form(); } /** * One document as HTML: the template with its placeholders filled, every value escaped. $c empty: the page version, with * the buyer's parts left for the order. Live (the checkout): the buyer's parts carry data-storesuit-legal-field. */ public static function document( string $type, array $c ): string { $s = self::seller(); $e = static fn( $v ): string => esc_html( (string) $v ); $later = 'Sipariş sırasında alıcının girdiği bilgilerle doldurulur.'; $value = static function ( string $key, string $v ) use ( $c, $e, $later ): string { if ( ! $c ) { return '' . $e( $later ) . ''; } $shown = $v !== '' ? $e( $v ) : '' . $e( 'Henüz girilmedi' ) . ''; return ! empty( $c['live'] ) ? '' . $shown . '' : $shown; }; $money = static fn( float $n ): string => wp_kses_post( wc_price( $n, array( 'currency'=>$c['currency'] ?? get_woocommerce_currency() ) ) ); $site = esc_url( home_url( '/' ) ); $withdraw = max( 14, (int) ( $s['withdrawal_days'] ?: 14 ) ); // the store's return period, never under the legal 14 days $tax = trim( ( $s['tax_office'] !== '' ? $s['tax_office'] . ' V.D. ' : '' ) . $s['tax_number'] ); $row = static fn( string $label, string $html ): string => '' . $e( 'Sipariş edilen ürünler; temel nitelikleri, adetleri, vergiler dahil fiyatları, teslimat bedeli, ödeme yöntemi ve toplam tutar sipariş sırasında burada gösterilir.' ) . '
'; if ( $c ) { // The goods and the price: each line's main features (its options), quantity and price with taxes, then the totals. $goods = '| Ürün ve temel nitelikleri | Adet | Birim fiyat (KDV dahil) | Tutar (KDV dahil) |
|---|---|---|---|
| ' . $e( $line['name'] ) . ( $line['details'] !== '' ? ' ' . $e( $line['details'] ) . '' : '' ) . ' | ' . $e( wc_stock_amount( $line['qty'] ) ) . ' | ' . $money( (float) $line['unit'] ) . ' | ' . $money( (float) $line['total'] ) . ' |
Alıcı, cayma bildirimini yönelttiği tarihten itibaren 14 gün içinde malı ' . $return_to . ' adresine gönderir. Satıcı iade için belirli bir taşıyıcı belirlememiştir; iade gönderiminin masrafı alıcıdan talep edilmez. Satıcı, cayma bildirimi üzerine alıcıya iade için kullanabileceği kargo firmasını ve varsa iade kodunu bildirir' . ( $s['return_code'] !== '' ? ' (anlaşmalı iade kodu: ' . $e( $s['return_code'] ) . ')' : '' ) . '.
' : 'Alıcı, cayma bildirimini yönelttiği tarihten itibaren 14 gün içinde malı, satıcının iade için belirlediği taşıyıcı ' . $carrier . ' ile ' . $return_to . ' adresine gönderir. Bu taşıyıcı ile yapılan iadede iade masrafı alıcıdan istenmez. Belirtilen taşıyıcının alıcının bulunduğu yerde şubesi yoksa satıcı malın ek masrafsız alınmasını sağlar. Alıcının malı başka bir taşıyıcı ile göndermesi halinde gönderim masrafı alıcıya aittir ve bedel iadesi süresi malın satıcıya ulaştığı tarihten başlar.
', 'refund_start'=>$s['return_open'] === '1' ? 'taşıyıcıya teslim edildiği' : 'belirtilen taşıyıcıya teslim edildiği (başka bir taşıyıcıyla gönderilmişse satıcıya ulaştığı)', 'withdrawal_notice'=>'Cayma bildirimi yazılı olarak veya kalıcı veri saklayıcısı ile (e-posta dahil) ' . $e( $s['email'] ) . ' adresine' . ( $s['kep'] !== '' ? ', ' . $e( $s['kep'] ) . ' KEP adresine' : '' ) . ' ya da ' . $e( $s['address'] ) . ' adresine iletilir.', 'exceptions_list'=>'Taraflarca aksi kararlaştırılmadıkça alıcı, aşağıdaki sözleşmelerde cayma hakkını kullanamaz (Yönetmelik m.15):
' . nl2br( $e( $s['exceptions'] ) ) . '
' : '' ), 'complaint_contacts'=>$e( $s['phone'] ) . ' numaralı telefondan ve ' . $e( $s['email'] ) . ' e-posta adresinden iletilebilir' . ( $s['complaint'] !== '' ? '; ayrıca: ' . $e( $s['complaint'] ) : '' ), 'privacy_link'=>$privacy && get_post_status( $privacy ) === 'publish' ? '; ayrıntılar ' . $e( get_the_title( $privacy ) ) . ' sayfasındadır' : '', 'sample_form'=>'(Bu form, yalnızca sözleşmeden cayma hakkı kullanılmak istendiğinde doldurulup gönderilir.)
Satıcı: ' . $e( $s['title'] ) . ' · Alıcı: ' . $value( 'name', (string) ( $b['name'] ?? '' ) ) . ' · Tarih: ' . $e( $c['date'] ) . '
' : '', ); $map = array(); foreach ( $vars as $key => $html ) { $map[ '{' . $key . '}' ] = $html; } return strtr( self::template( $type ), $map ); } }