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 = ''; } /** m.8: the button says that the order means paying. */ public static function button_text( $text ) { return self::turkish() && self::pages() ? 'Siparişi onayla ve öde' : $text; } public static function assets(): void { if ( ! function_exists( 'is_checkout' ) || ( ! is_checkout() && ! is_account_page() ) || ! self::pages() ) { return; } $base = plugins_url( 'assets/', STORESUIT_FILE ); $dir = STORESUIT_DIR . 'assets/'; // The order pages (confirmation, My Account) show the documents the order kept; the window is the checkout's. wp_enqueue_style( 'storesuit-checkout-legal', $base . 'checkout-legal.css', array(), STORESUIT_VERSION . '.' . (string) filemtime( $dir . 'checkout-legal.css' ) ); if ( ! is_checkout() || is_order_received_page() ) { return; } wp_enqueue_script( 'storesuit-checkout-legal', $base . 'checkout-legal.js', array(), STORESUIT_VERSION . '.' . (string) filemtime( $dir . 'checkout-legal.js' ), array( 'in_footer'=>true, 'strategy'=>'defer' ) ); wp_localize_script( 'storesuit-checkout-legal', 'StoreSuitLegal', array( 'endpoint'=>class_exists( 'WC_AJAX' ) ? \WC_AJAX::get_endpoint( 'storesuit_legal_document' ) : '', 'close'=>I18n::t( 'Close' ), 'print'=>I18n::t( 'Print' ), 'read'=>I18n::t( 'I have read it' ), 'loading'=>I18n::t( 'Loading…' ), 'error'=>I18n::t( 'The document could not be opened. Open it on its own page.' ), ) ); // WooCommerce redraws the order summary (and this box) when the address or shipping changes: the tick stays. if ( wp_script_is( 'wc-checkout', 'enqueued' ) ) { wp_add_inline_script( 'wc-checkout', 'jQuery(function($){var on=false;$(document.body).on("change","#storesuit_legal_consent",function(){on=this.checked}).on("updated_checkout",function(){if(on)$("#storesuit_legal_consent").prop("checked",true)})});' ); } } public static function classic_check(): void { $pages = self::pages(); if ( ! $pages || ! empty( $_POST['storesuit_legal_consent'] ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification -- WooCommerce checks its own nonce. $titles = wp_list_pluck( $pages, 'title' ); wc_add_notice( count( $titles ) > 1 ? I18n::format( 'Please accept the %1$s and the %2$s to place your order.', $titles[0], $titles[1] ) : I18n::format( 'Please accept the %s to place your order.', $titles[0] ), 'error' ); } public static function classic_record( $order ): void { if ( $order instanceof \WC_Order && ! empty( $_POST['storesuit_legal_consent'] ) ) { self::record( $order, 'classic' ); } // phpcs:ignore WordPress.Security.NonceVerification -- WooCommerce checks its own nonce. } /** After WooCommerce saved the order (its number is known): the documents as accepted, their hashes and the note. */ public static function classic_snapshot( $order ): void { if ( $order instanceof \WC_Order && is_array( $order->get_meta( self::META ) ) && self::snapshot( $order ) ) { $order->save(); self::note( $order ); } } public static function block_record( $order ): void { if ( ! $order instanceof \WC_Order || ! in_array( $order->get_meta( '_wc_other/' . self::FIELD ), array( true, 1, '1' ), true ) ) { return; } self::record( $order, 'block' ); $kept = self::snapshot( $order ); $order->save(); if ( $kept ) { self::note( $order ); } } /** A short fingerprint of the seller details a generated document was written from (its version). */ public static function seller_version(): string { return substr( hash( 'sha256', (string) wp_json_encode( self::seller() ) ), 0, 12 ); } /** * The acceptance, once per order: when (UTC and the store's time), from where (the IP and browser WooCommerce saved on * the order; nothing else is collected), the box's exact words, classic or block checkout, and each document's version * (a page: its ID and last change; a generated document: the seller details' fingerprint). */ private static function record( \WC_Order $order, string $checkout ): void { $kept = $order->get_meta( self::META ); if ( is_array( $kept ) && ! empty( $kept['hashes'] ) ) { return; } // written once $generated = self::generated(); $pages = array(); foreach ( self::pages() as $page ) { $key = (string) ( $page['key'] ?? '' ); $id = (int) ( $page['id'] ?? 0 ); $pages[] = array( 'key'=>$key !== '' ? $key : 'page-' . $id, 'id'=>$id, 'title'=>$page['title'], 'modified'=>$id ? (string) get_post_field( 'post_modified_gmt', $id ) : '', 'source'=>$generated && isset( self::TITLES[ $key ] ) ? 'generated' : 'page', 'template'=>$generated && isset( self::TITLES[ $key ] ) ? self::template_version( $key ) : null ); } if ( ! $pages ) { return; } $time = time(); $order->update_meta_data( self::META, array( 'time'=>$time, 'time_utc'=>gmdate( 'Y-m-d H:i:s', $time ) . ' UTC', 'time_local'=>wp_date( 'Y-m-d H:i:s T', $time ), 'ip'=>(string) ( $order->get_customer_ip_address() ?: \WC_Geolocation::get_ip_address() ), 'user_agent'=>(string) ( $order->get_customer_user_agent() ?: wc_get_user_agent() ), 'label'=>self::sentence( false ), 'checkout'=>$checkout, 'pages'=>$pages, 'seller_version'=>$generated ? self::seller_version() : '', ) ); } /** * The documents as the buyer accepted them, with a SHA-256 of each, written once (kept three years, m.20); a later save * never writes them again. A generated document is written from the order: the seller details, buyer, lines and totals * the checkout window showed, with the order number and time added. Without generated documents, the published pages * as they read at that moment. */ private static function snapshot( \WC_Order $order ): bool { $kept = $order->get_meta( self::DOCUMENTS ); if ( is_array( $kept ) && ! empty( $kept['documents'] ) ) { return false; } $record = $order->get_meta( self::META ); $documents = array(); $titles = array(); if ( self::generated() ) { $context = self::order_context( $order ); foreach ( self::TITLES as $type => $title ) { $documents[ $type ] = self::document( $type, $context ); $titles[ $type ] = $title; } $basis = 'Written from the order when it was placed: the seller details, buyer details, lines and totals the checkout window showed, with the order number and time added.'; } else { foreach ( is_array( $record ) ? (array) ( $record['pages'] ?? array() ) : array() as $page ) { $post = ! empty( $page['id'] ) ? get_post( (int) $page['id'] ) : null; if ( $post instanceof \WP_Post ) { $documents[ $page['key'] ] = (string) apply_filters( 'the_content', $post->post_content ); $titles[ $page['key'] ] = (string) $page['title'] ; } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- WordPress's own filter. } $basis = 'The published pages as they read when the order was placed.'; } if ( ! $documents ) { return false; } $hashes = array_map( static fn( string $html ): string => hash( 'sha256', $html ), $documents ); $order->update_meta_data( self::DOCUMENTS, array( 'time'=>time(), 'documents'=>$documents, 'titles'=>$titles, 'hashes'=>$hashes, 'basis'=>$basis ) ); if ( is_array( $record ) ) { $record['hashes'] = $hashes; $order->update_meta_data( self::META, $record ); } return true; } /** A system note on the order (timestamped, in its history): what was accepted, when, from which IP, with the hashes. */ private static function note( \WC_Order $order ): void { $record = $order->get_meta( self::META ); $kept = $order->get_meta( self::DOCUMENTS ); if ( ! is_array( $record ) || ! is_array( $kept ) || empty( $kept['hashes'] ) ) { return; } $titles = array_values( (array) ( $kept['titles'] ?? array() ) ); $short = implode( ' / ', array_map( static fn( string $h ): string => substr( $h, 0, 12 ), (array) $kept['hashes'] ) ); $order->add_order_note( I18n::format( '%1$s accepted · %2$s · IP %3$s · SHA-256 %4$s', implode( ' + ', $titles ), (string) $record['time_local'], (string) ( $record['ip'] ?: '—' ), $short ), 0, false ); } /** The documents still match what was saved: every stored hash against the stored text. */ public static function intact( \WC_Order $order ): ?bool { $kept = $order->get_meta( self::DOCUMENTS ); if ( ! is_array( $kept ) || empty( $kept['hashes'] ) ) { return null; } foreach ( (array) $kept['hashes'] as $type => $hash ) { if ( ! is_string( $kept['documents'][ $type ] ?? null ) || ! hash_equals( (string) $hash, hash( 'sha256', $kept['documents'][ $type ] ) ) ) { return false; } } return true; } /** Block checkout: the field's label is plain text, so the documents are linked under the form. */ public static function block_links( $html ) { $pages = self::pages(); if ( ! $pages || ! is_string( $html ) ) { return $html; } $links = implode( ' · ', array_map( array( __CLASS__, 'opener' ), $pages ) ); return $html . ''; } /** * 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 ''; } /** A kept document's title: as recorded, else the regulation's name. */ private static function title( array $kept, string $type ): string { return (string) ( $kept['titles'][ $type ] ?? ( self::TITLES[ $type ] ?? $type ) ); } /** The documents the order kept, each folded under its title. */ private static function kept( \WC_Order $order ): string { $kept = $order->get_meta( self::DOCUMENTS ); if ( ! is_array( $kept ) || empty( $kept['documents'] ) ) { return ''; } $html = ''; foreach ( (array) $kept['documents'] as $type => $document ) { if ( is_string( $document ) ) { $html .= ''; } } return $html; } /* ---------- The record on the order screen, the evidence file ---------- */ public static function can_view_evidence(): bool { return current_user_can( 'manage_woocommerce' ) || current_user_can( 'edit_shop_orders' ); } /** "Contract record" on the order screen (classic and HPOS order screens) for orders that have one. */ public static function meta_box( $screen = '', $object = null ): void { $order = $object instanceof \WC_Order ? $object : ( $object instanceof \WP_Post ? wc_get_order( $object->ID ) : null ); if ( ! $order instanceof \WC_Order || ! is_array( $order->get_meta( self::META ) ) || ! self::can_view_evidence() ) { return; } $screens = array( 'shop_order' ); if ( function_exists( 'wc_get_page_screen_id' ) ) { $screens[] = wc_get_page_screen_id( 'shop-order' ); } foreach ( array_unique( $screens ) as $id ) { add_meta_box( 'storesuit-legal-record', I18n::t( 'Contract record' ), array( __CLASS__, 'record_box' ), $id, 'normal', 'default' ); } } public static function record_box( $object ): void { $order = $object instanceof \WC_Order ? $object : ( $object instanceof \WP_Post ? wc_get_order( $object->ID ) : null ); if ( ! $order instanceof \WC_Order ) { return; } echo self::record_html( $order, true ); // phpcs:ignore WordPress.Security.EscapeOutput -- escaped in record_html(). } /** The record as a table (the order screen and the evidence file share it), every value escaped. */ public static function record_html( \WC_Order $order, bool $screen ): string { $r = $order->get_meta( self::META ); $kept = $order->get_meta( self::DOCUMENTS ); $mail = $order->get_meta( '_storesuit_legal_emailed' ); if ( ! is_array( $r ) ) { return ''; } $kept = is_array( $kept ) ? $kept : array(); $row = static fn( string $label, string $html ): string => '' . esc_html( $label ) . '' . $html . ''; $html = ''; $html .= $row( I18n::t( 'Accepted at' ), esc_html( (string) ( $r['time_local'] ?? wp_date( 'Y-m-d H:i:s T', (int) ( $r['time'] ?? 0 ) ) ) ) . ( ! empty( $r['time_utc'] ) ? ' · ' . esc_html( (string) $r['time_utc'] ) : '' ) ); $html .= $row( I18n::t( 'IP address' ), esc_html( (string) ( $r['ip'] ?? '' ) ?: '—' ) ); $html .= $row( I18n::t( 'Browser' ), esc_html( (string) ( $r['user_agent'] ?? '' ) ?: '—' ) ); $html .= $row( I18n::t( 'Checkout' ), esc_html( ( $r['checkout'] ?? '' ) === 'block' ? I18n::t( 'Block checkout' ) : ( ( $r['checkout'] ?? '' ) === 'classic' ? I18n::t( 'Classic checkout' ) : '—' ) ) ); $html .= $row( I18n::t( 'Box the customer ticked' ), esc_html( (string) ( $r['label'] ?? '' ) ?: '—' ) ); foreach ( (array) ( $r['pages'] ?? array() ) as $page ) { $key = (string) ( $page['key'] ?? '' ); $hash = (string) ( $kept['hashes'][ $key ] ?? '' ); $version = ( $page['source'] ?? '' ) === 'generated' ? I18n::format( 'written from the contract text version %1$s and the seller details (version %2$s)', isset( $page['template'] ) ? ( (int) $page['template'] === 0 ? I18n::t( 'StoreRuler default' ) : (string) (int) $page['template'] ) : '—', (string) ( $r['seller_version'] ?? '' ) ) :I18n::format( 'page #%1$s, last changed %2$s UTC', (string) ( $page['id'] ?? '' ), (string) ( $page['modified'] ?? '' ) ); $html .= $row( (string) ( $page['title'] ?? $key ), esc_html( $version ) . ( $hash !== '' ? '
SHA-256 ' . esc_html( $hash ) . '' : '' ) ); } $intact = self::intact( $order ); $html .= $row( I18n::t( 'Integrity' ), $intact === null ? esc_html( I18n::t( 'No copy of the documents was kept with this order.' ) ) : ( $intact ? esc_html( I18n::t( 'The kept documents match their hashes.' ) ) : '' . esc_html( I18n::t( 'The kept documents no longer match their hashes.' ) ) . '' ) ); if ( ! empty( $kept['basis'] ) ) { $html .= $row( I18n::t( 'Written from' ), esc_html( I18n::t( (string) $kept['basis'] ) ) ); } $html .= $row( I18n::t( 'Sent by e-mail' ), is_array( $mail ) ? esc_html( I18n::format( '%1$s to %2$s (%3$s)', wp_date( 'Y-m-d H:i:s T', (int) $mail['time'] ), (string) $mail['to'], (string) $mail['email'] ) ) : ( $mail ? esc_html( wp_date( 'Y-m-d H:i:s T', (int) $mail ) ) : '' . esc_html( I18n::t( 'Not sent yet: no customer order e-mail has carried the documents. Use the order action "Send the accepted contracts to the customer".' ) ) . '' ) ); $html .= ''; if ( $screen && ! empty( $kept['documents'] ) ) { $url = wp_nonce_url( admin_url( 'admin-post.php?action=storesuit_legal_evidence&order=' . $order->get_id() ), 'storesuit_legal_evidence_' . $order->get_id() ); $html .= '

' . 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( I18n::format( 'Contract record · order #%s', $order->get_order_number() ) ) . ''; $html .= '

' . esc_html( I18n::format( 'Contract record · order #%s', $order->get_order_number() ) ) . '

' . 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 .= '

' . esc_html( self::title( $kept, (string) $type ) ) . '

SHA-256 ' . esc_html( (string) ( $kept['hashes'][ $type ] ?? hash( 'sha256', $document ) ) ) . '

' . $document . '
'; } return $html . ''; } /** admin-post.php?action=storesuit_legal_evidence: the evidence file as a download, for those who manage orders. */ public static function evidence_download(): void { $id = absint( $_GET['order'] ?? 0 ); // phpcs:ignore WordPress.Security.NonceVerification -- checked on the next line. check_admin_referer( 'storesuit_legal_evidence_' . $id ); $order = $id ? wc_get_order( $id ) : null; if ( ! self::can_view_evidence() || ! $order instanceof \WC_Order || ! is_array( $order->get_meta( self::DOCUMENTS ) ) ) { wp_die( esc_html( I18n::t( 'You do not have permission to perform this action.' ) ), 403 ); } nocache_headers(); header( 'Content-Type: text/html; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="contract-record-' . sanitize_file_name( (string) $order->get_order_number() ) . '.html"' ); echo self::evidence( $order ); // phpcs:ignore WordPress.Security.EscapeOutput -- the kept documents were escaped when written; the rest is escaped in evidence(). exit; } /** An order whose documents no customer e-mail carried yet (a payment left pending, an e-mail turned off): send them. */ public static function order_actions( $actions, $order = null ) { if ( is_array( $actions ) && $order instanceof \WC_Order && ! $order->get_meta( '_storesuit_legal_emailed' ) && is_array( $order->get_meta( self::DOCUMENTS ) ) && $order->get_billing_email() ) { $actions['storesuit_send_legal'] = I18n::t( 'Send the accepted contracts to the customer' ); } return $actions; } public static function send_documents( $order ): bool { $kept = $order instanceof \WC_Order ? $order->get_meta( self::DOCUMENTS ) : null; if ( ! is_array( $kept ) || empty( $kept['documents'] ) || ! $order->get_billing_email() ) { return false; } $mailer = WC()->mailer(); $body = ''; foreach ( (array) $kept['documents'] as $document ) { if ( is_string( $document ) ) { $body .= '
' . wp_kses_post( $document ) . '
'; } } $subject = I18n::format( 'Your contracts for order #%s', $order->get_order_number() ); $sent = $mailer->send( $order->get_billing_email(), $subject, $mailer->wrap_message( $subject, $body ) ); if ( $sent ) { self::mailed( $order, 'storesuit_legal_documents', $order->get_billing_email() ); } return (bool) $sent; } /** Which e-mail carried the documents, to whom, when, with the hashes it carried; and a note on the order. */ private static function mailed( \WC_Order $order, string $email, string $to ): void { $kept = $order->get_meta( self::DOCUMENTS ); $hashes = is_array( $kept ) ? (array) ( $kept['hashes'] ?? array() ) : array(); $order->update_meta_data( '_storesuit_legal_emailed', array( 'email'=>$email, 'to'=>$to, 'time'=>time(), 'hashes'=>$hashes ) ); $order->save_meta_data(); $order->add_order_note( I18n::format( 'The accepted contracts were sent to %1$s (%2$s).', $to, $email ), 0, false ); } /** A privacy eraser anonymising the order keeps the contract record (a legal duty for 3 years, m.20) and says so. */ public static function erasing( $order ): void { if ( $order instanceof \WC_Order && is_array( $order->get_meta( self::META ) ) ) { $order->add_order_note( I18n::t( 'Personal data was removed from this order. The contract record (time, IP, browser, the documents and their hashes) is kept for the 3-year legal period.' ), 0, false ); } } /** WooCommerce's "retain completed orders" in days, 0 when orders are kept. */ public static function retention_days(): int { $setting = get_option( 'woocommerce_anonymize_completed_orders', array() ); $number = is_array( $setting ) ? absint( $setting['number'] ?? 0 ) : 0; $unit = is_array( $setting ) ? (string) ( $setting['unit'] ?? 'days' ) : 'days'; return $number * ( array( 'days'=>1, 'weeks'=>7, 'months'=>30, 'years'=>365 )[ $unit ] ?? 1 ); } /** The customer's order page (My Account and the confirmation): the documents they accepted. */ public static function customer_documents( $order ): void { if ( ! $order instanceof \WC_Order ) { return; } $kept = self::kept( $order ); if ( $kept !== '' ) { echo ''; } // phpcs:ignore WordPress.Security.EscapeOutput -- see kept(). } /** The order e-mail to the customer carries the documents (a durable medium, m.6/1), once. */ public static function email_documents( $order, $sent_to_admin = false, $plain_text = false, $email = null ): void { if ( $sent_to_admin || ! $order instanceof \WC_Order || ! is_object( $email ) || ! in_array( $email->id ?? '', array( 'customer_on_hold_order', 'customer_processing_order' ), true ) ) { return; } $kept = $order->get_meta( self::DOCUMENTS ); if ( ! is_array( $kept ) || empty( $kept['documents'] ) || $order->get_meta( '_storesuit_legal_emailed' ) ) { return; } foreach ( (array) $kept['documents'] as $type => $document ) { if ( ! is_string( $document ) ) { continue; } if ( $plain_text ) { echo "\n\n" . esc_html( mb_strtoupper( self::title( $kept, (string) $type ) ) ) . "\n\n" . esc_html( trim( html_entity_decode( wp_strip_all_tags( str_replace( array( '

', '', '', '', '
' ), "\n", $document ) ), ENT_QUOTES, 'UTF-8' ) ) ) . "\n"; } else { echo '
' . wp_kses_post( $document ) . '
'; } } self::mailed( $order, (string) $email->id, (string) ( is_callable( array( $email, 'get_recipient' ) ) ? $email->get_recipient() : $order->get_billing_email() ) ); } /* ---------- What the documents are written from ---------- */ /** An address as one line: street, postcode, district, province, country. */ private static function address( array $a ): string { $countries = WC()->countries; $country = (string) ( $a['country'] ?? '' ); $states = $country !== '' ? $countries->get_states( $country ) : array(); $state = (string) ( $a['state'] ?? '' ); $region = is_array( $states ) && isset( $states[ $state ] ) ? $states[ $state ] : $state; $names = $countries->get_countries(); return implode( ', ', array_filter( array_map( 'trim', array( trim( ( $a['address_1'] ?? '' ) . ' ' . ( $a['address_2'] ?? '' ) ), trim( ( $a['postcode'] ?? '' ) . ' ' . ( $a['city'] ?? '' ) ), $region, $country !== 'TR' ? ( $names[ $country ] ?? $country ) : '' ) ) ) ); } /** The checkout now: the form's fields (else the customer session), the cart's lines and totals, the chosen payment. */ private static function cart_context( array $posted ): array { $customer = WC()->customer; $cart = WC()->cart; $get = static function ( string $key ) use ( $posted, $customer ): string { if ( array_key_exists( $key, $posted ) ) { return (string) $posted[ $key ]; } $method = 'get_' . $key; return $customer && is_callable( array( $customer, $method ) ) ? (string) $customer->$method() : ''; }; $billing = array(); $shipping = array(); foreach ( array( 'first_name', 'last_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country', 'phone', 'email' ) as $field ) { $billing[ $field ] = $get( 'billing_' . $field ); } $different = ! empty( $posted['ship_to_different_address'] ); foreach ( array( 'first_name', 'last_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country' ) as $field ) { $shipping[ $field ] = $different ? $get( 'shipping_' . $field ) : $billing[ $field ]; } $lines = array(); if ( $cart ) { foreach ( $cart->get_cart() as $item ) { $product = $item['data'] ?? null; if ( ! $product instanceof \WC_Product ) { continue; } $qty = (float) $item['quantity']; $total = (float) $item['line_total'] + (float) $item['line_tax']; $lines[] = array( 'name'=>Store::plain( $product->get_name() ), 'details'=>trim( str_replace( "\n", ' · ', wp_strip_all_tags( wc_get_formatted_cart_item_data( $item, true ) ) ) ), 'qty'=>$qty, 'unit'=>$qty > 0 ? $total / $qty : 0.0, 'total'=>$total ); } } $methods = array(); foreach ( WC()->shipping() ? WC()->shipping()->get_packages() : array() as $index => $package ) { $chosen = WC()->session ? ( (array) WC()->session->get( 'chosen_shipping_methods' ) )[ $index ] ?? '' : ''; if ( $chosen !== '' && isset( $package['rates'][ $chosen ] ) ) { $methods[] = wp_strip_all_tags( $package['rates'][ $chosen ]->get_label() ); } } $gateway = (string) ( $posted['payment_method'] ?? ( WC()->session ? WC()->session->get( 'chosen_payment_method' ) : '' ) ); $gateways = WC()->payment_gateways()->payment_gateways(); return array( 'buyer'=>array( 'name'=>trim( $billing['first_name'] . ' ' . $billing['last_name'] ), 'company'=>$billing['company'], 'phone'=>$billing['phone'], 'email'=>$billing['email'], 'billing'=>self::address( $billing ), 'delivery'=>self::address( $shipping ), 'recipient'=>trim( $shipping['first_name'] . ' ' . $shipping['last_name'] ) ), 'lines'=>$lines, 'currency'=>get_woocommerce_currency(), 'subtotal'=>$cart ? (float) $cart->get_subtotal() + (float) $cart->get_subtotal_tax() : 0.0, 'discount'=>$cart ? (float) $cart->get_discount_total() + (float) $cart->get_discount_tax() : 0.0, 'shipping'=>$cart ? (float) $cart->get_shipping_total() + (float) $cart->get_shipping_tax() : 0.0, 'shipping_method'=>implode( ', ', $methods ), 'fees'=>$cart ? array_sum( array_map( static fn( $fee ) => (float) $fee->total + (float) $fee->tax, $cart->get_fees() ) ) : 0.0, 'tax'=>$cart ? (float) $cart->get_total_tax() : 0.0, 'total'=>$cart ? (float) $cart->get_total( 'edit' ) : 0.0, 'payment'=>isset( $gateways[ $gateway ] ) ? wp_strip_all_tags( $gateways[ $gateway ]->get_title() ) : '', 'date'=>wp_date( 'd.m.Y H:i' ), 'number'=>'', 'live'=>true, ); } /** A placed order, as it was saved. */ private static function order_context( \WC_Order $order ): array { $billing = array(); $shipping = array(); foreach ( array( 'first_name', 'last_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country', 'phone', 'email' ) as $field ) { $method = 'get_billing_' . $field; $billing[ $field ] = (string) $order->$method(); } foreach ( array( 'first_name', 'last_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country' ) as $field ) { $method = 'get_shipping_' . $field; $shipping[ $field ] = (string) $order->$method(); } if ( trim( $shipping['address_1'] ) === '' ) { $shipping = $billing; } $lines = array(); foreach ( $order->get_items() as $item ) { if ( ! $item instanceof \WC_Order_Item_Product ) { continue; } $details = array(); foreach ( $item->get_formatted_meta_data() as $meta ) { $details[] = wp_strip_all_tags( $meta->display_key ) . ': ' . wp_strip_all_tags( (string) $meta->display_value ); } $qty = (float) $item->get_quantity(); $total = (float) $item->get_total() + (float) $item->get_total_tax(); $lines[] = array( 'name'=>Store::plain( $item->get_name() ), 'details'=>implode( ' · ', $details ), 'qty'=>$qty, 'unit'=>$qty > 0 ? $total / $qty : 0.0, 'total'=>$total ); } $created = $order->get_date_created(); return array( 'buyer'=>array( 'name'=>trim( $billing['first_name'] . ' ' . $billing['last_name'] ), 'company'=>$billing['company'], 'phone'=>$billing['phone'], 'email'=>$billing['email'], 'billing'=>self::address( $billing ), 'delivery'=>self::address( $shipping ), 'recipient'=>trim( $shipping['first_name'] . ' ' . $shipping['last_name'] ) ), 'lines'=>$lines, 'currency'=>$order->get_currency(), 'subtotal'=>(float) $order->get_subtotal() + array_sum( array_map( static fn( $i ) => (float) $i->get_subtotal_tax(), array_filter( $order->get_items(), static fn( $i ) => $i instanceof \WC_Order_Item_Product ) ) ), 'discount'=>(float) $order->get_discount_total() + (float) $order->get_discount_tax(), 'shipping'=>(float) $order->get_shipping_total() + (float) $order->get_shipping_tax(), 'shipping_method'=>$order->get_shipping_method(), 'fees'=>array_sum( array_map( static fn( $fee ) => (float) $fee->get_total() + (float) $fee->get_total_tax(), $order->get_fees() ) ), 'tax'=>(float) $order->get_total_tax(), 'total'=>(float) $order->get_total(), 'payment'=>wp_strip_all_tags( $order->get_payment_method_title() ), 'date'=>$created ? $created->date_i18n( 'd.m.Y H:i' ) : wp_date( 'd.m.Y H:i' ), 'number'=>(string) $order->get_order_number(), 'live'=>false, ); } /* ---------- The documents (Turkish law: Turkish text), editable templates ---------- */ public const TEMPLATES = 'storesuit_legal_templates'; public const HISTORY = 'storesuit_legal_template_history'; public const DISCLAIMER = 'storesuit_legal_disclaimer'; /** The placeholders a template may use, with what each becomes (shown beside the editor). */ public const PLACEHOLDERS = array( 'seller_table'=>'Seller details as a table (title, MERSİS or tax number, address, phone, e-mail, KEP, website)', 'buyer_table'=>'Buyer details as a table (name, delivery and billing address, phone, e-mail), filled at checkout', 'items_table'=>'The products with their options, quantities and prices, then shipping, discounts, total, VAT, payment method and order date', 'seller_title'=>'Seller name or title', 'seller_tax'=>'Tax office and number', 'seller_mersis'=>'MERSİS number', 'seller_address'=>'Seller address', 'seller_phone'=>'Seller phone', 'seller_email'=>'Seller e-mail', 'seller_kep'=>'KEP address', 'site_url'=>'Store address (URL)', 'buyer_name'=>'Buyer name', 'buyer_address'=>'Delivery address', 'buyer_billing'=>'Billing address', 'buyer_phone'=>'Buyer phone', 'buyer_email'=>'Buyer e-mail', 'total'=>'Order total with taxes', 'payment_method'=>'Payment method', 'order_date'=>'Order date', 'order_number'=>'Order number', 'return_days'=>'Return period ("14 (on dört) gün" or the longer period you set)', 'return_days_note'=>'Note added when your period is longer than 14 days', 'dispatch_days'=>'Days to hand the order to the carrier', 'return_carrier'=>'Carrier for returns', 'return_address'=>'Return address', 'return_shipping'=>'The return shipping paragraph (named carrier, or none named: free for the buyer)', 'refund_start'=>'When the refund period starts (depends on the carrier choice)', 'withdrawal_notice'=>'Where the withdrawal notice is sent (e-mail, KEP, address)', 'exceptions_list'=>'The exceptions to the right of withdrawal, with your own note', 'complaint_contacts'=>'The complaint channels (phone, e-mail, other contact)', 'privacy_link'=>'Link to your privacy page, when there is one', 'sample_form'=>'The sample withdrawal form', 'preinfo_title'=>'"Ön Bilgilendirme Formu"', 'signature'=>'Seller, buyer and date line (orders only)', ); /** Sections a document must keep (Yönetmelik m.5): taking one out is warned, not refused. */ private const REQUIRED = array( 'seller_table', 'items_table', 'return_days', 'return_shipping', 'exceptions_list', 'complaint_contacts' ); /** The template as written by StoreRuler (Turkish), the starting point and what "restore" brings back. */ public static function default_template( string $type ): string { $delivery = '

Ü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 '

ÖN BİLGİLENDİRME FORMU

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.

' . '

1. Satıcı bilgileri

{seller_table}

2. Alıcı bilgileri

{buyer_table}' . '

3. Sözleşme konusu ürünler, fiyat ve ödeme

{items_table}

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.

' . '

4. Teslimat

' . $delivery . '

5. Cayma hakkı

' . $withdrawal . '

6. Cayma hakkının kullanılamayacağı haller

{exceptions_list}' . '

7. Şikâyet ve itiraz yolları

' . $complaints . '

8. Kişisel veriler ve kayıtlar

' . $data . '

9. Siparişin onayı

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 '

MESAFELİ SATIŞ SÖZLEŞMESİ

' . '

Madde 1 – Taraflar

Satıcı

{seller_table}

Alıcı

{buyer_table}' . '

Madde 2 – Konu

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.

' . '

Madde 3 – Sözleşme konusu ürünler, ödeme ve teslimat

{items_table}

Fatura, alıcının sipariş sırasında bildirdiği fatura bilgilerine düzenlenir ve ürünle birlikte veya elektronik ortamda iletilir.

' . '

Madde 4 – Genel hükümler

    ' . '
  1. Alıcı, {preinfo_title}\'nu okuyup bilgi sahibi olduğunu ve elektronik ortamda teyit ettiğini kabul eder. Ön bilgilendirme formu bu sözleşmenin ayrılmaz bir parçasıdır.
  2. ' . '
  3. Satıcı, ürünü sağlam, eksiksiz, siparişte belirtilen niteliklere uygun ve varsa garanti belgeleri ve kullanım kılavuzlarıyla teslim eder.
  4. ' . '
  5. Alıcı, sipariş sırasında verdiği ad, adres, telefon ve e-posta bilgilerinin doğru ve güncel olduğunu; yanlış veya eksik bilgi nedeniyle teslimatın gecikmesinden ya da yapılamamasından satıcının sorumlu olmadığını kabul eder. Alıcının adreste bulunmaması veya haklı bir sebep olmaksızın teslim almaması nedeniyle satıcıya geri dönen ürünün yeniden gönderim masrafı alıcıya aittir.
  6. ' . '
  7. Ödeme, ödeme adımında seçilen yöntemle yapılır. Kartla ödemelerde ödeme onaylanmadan sipariş kargoya verilmez. Ürün teslim edildikten sonra alıcıya ait kartın yetkisiz kişilerce haksız veya hukuka aykırı kullanılması nedeniyle ilgili banka veya finans kuruluşu ürün bedelini satıcıya ödemezse, alıcı ürünü 3 gün içinde, gönderim masrafı kendisine ait olmak üzere satıcıya iade eder.
  8. ' . '
  9. Doğal afet, salgın, savaş, grev, ulaşım engeli gibi tarafların kontrolü dışındaki mücbir sebeplerle teslimat gecikirse satıcı durumu alıcıya bildirir; alıcı siparişi iptal edebilir veya teslimatı bekleyebilir. İptal halinde tahsil edilen tüm ödemeler 14 gün içinde iade edilir.
  10. ' . '
  11. Ayıplı mallarda alıcının 6502 sayılı Kanunun 11. maddesindeki seçimlik hakları (bedel iadesi, ayıp oranında indirim, ücretsiz onarım veya ayıpsız misli ile değişim) saklıdır.
  12. ' . '
  13. Kartla taksitli işlemlerde alıcının temerrüdü halinde, kart sahibi ile banka arasındaki kredi kartı sözleşmesi hükümleri uygulanır.
' . '

Madde 5 – Teslimat

' . $delivery . '

Madde 6 – Cayma hakkı

' . $withdrawal . '

Madde 7 – Cayma hakkının istisnaları

{exceptions_list}' . '

Madde 8 – Uyuşmazlıklar

' . $complaints . '

Madde 9 – Kişisel veriler ve kayıtlar

' . $data . '

Madde 10 – Yürürlük

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 => '' . esc_html( $label ) . '' . $html . ''; $b = $c['buyer'] ?? array(); $goods = '

' . $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 = ''; foreach ( (array) ( $c['lines'] ?? array() ) as $line ) { $goods .= ''; } $goods .= '' . $row( 'Ürünler toplamı', $money( (float) $c['subtotal'] ) ) . ( (float) $c['discount'] > 0 ? $row( 'İndirim', '−' . $money( (float) $c['discount'] ) ) : '' ) . $row( 'Teslimat / kargo bedeli', (float) $c['shipping'] > 0 ? $money( (float) $c['shipping'] ) . ( $c['shipping_method'] !== '' ? ' (' . $e( $c['shipping_method'] ) . ')' : '' ) : $e( 'Ücretsiz' ) ) . ( abs( (float) $c['fees'] ) > 0.004 ? $row( 'Diğer ücretler', $money( (float) $c['fees'] ) ) : '' ) . $row( 'Toplam (tüm vergiler dahil)', '' . $money( (float) $c['total'] ) . '' ) . ( (float) $c['tax'] > 0 ? $row( 'Toplama dahil KDV', $money( (float) $c['tax'] ) ) : '' ) . $row( 'Ödeme yöntemi', $value( 'payment', (string) $c['payment'] ) ) . $row( 'Sipariş tarihi', $e( $c['date'] ) . ( $c['number'] !== '' ? ' · Sipariş no: ' . $e( $c['number'] ) : '' ) ) . ''; } $carrier = $e( $s['return_carrier'] ) . ( $s['return_code'] !== '' ? ' (anlaşmalı iade kodu: ' . $e( $s['return_code'] ) . ')' : '' ); $return_to = $e( $s['return_address'] !== '' ? $s['return_address'] : $s['address'] ); $privacy = (int) get_option( 'wp_page_for_privacy_policy', 0 ); $vars = array( 'seller_table'=>'' . $row( 'Unvan', $e( $s['title'] ) ) . ( $s['mersis'] !== '' ? $row( 'MERSİS No', $e( $s['mersis'] ) ) : '' ) . ( $tax !== '' ? $row( 'Vergi Dairesi / No', $e( $tax ) ) : '' ) . $row( 'Adres', $e( $s['address'] ) ) . $row( 'Telefon', $e( $s['phone'] ) ) . $row( 'E-posta', $e( $s['email'] ) ) . ( $s['kep'] !== '' ? $row( 'KEP adresi', $e( $s['kep'] ) ) : '' ) . $row( 'İnternet sitesi', $site ) . '', 'buyer_table'=>'' . $row( 'Ad Soyad / Unvan', $value( 'name', trim( ( $b['name'] ?? '' ) . ( ! empty( $b['company'] ) ? ' (' . $b['company'] . ')' : '' ) ) ) ) . $row( 'Teslimat adresi', $value( 'delivery', (string) ( $b['delivery'] ?? '' ) ) ) . $row( 'Fatura adresi', $value( 'billing', (string) ( $b['billing'] ?? '' ) ) ) . $row( 'Telefon', $value( 'phone', (string) ( $b['phone'] ?? '' ) ) ) . $row( 'E-posta', $value( 'email', (string) ( $b['email'] ?? '' ) ) ) . '', 'items_table'=>$goods, 'seller_title'=>$e( $s['title'] ), 'seller_tax'=>$e( $tax ), 'seller_mersis'=>$e( $s['mersis'] ), 'seller_address'=>$e( $s['address'] ), 'seller_phone'=>$e( $s['phone'] ), 'seller_email'=>$e( $s['email'] ), 'seller_kep'=>$e( $s['kep'] ), 'site_url'=>$site, 'buyer_name'=>$value( 'name', (string) ( $b['name'] ?? '' ) ), 'buyer_address'=>$value( 'delivery', (string) ( $b['delivery'] ?? '' ) ), 'buyer_billing'=>$value( 'billing', (string) ( $b['billing'] ?? '' ) ), 'buyer_phone'=>$value( 'phone', (string) ( $b['phone'] ?? '' ) ), 'buyer_email'=>$value( 'email', (string) ( $b['email'] ?? '' ) ), 'total'=>$c ? $money( (float) $c['total'] ) : $value( 'total', '' ), 'payment_method'=>$c ? $value( 'payment', (string) $c['payment'] ) : $value( 'payment', '' ), 'order_date'=>$c ? $e( $c['date'] ) : $value( 'date', '' ), 'order_number'=>$c && $c['number'] !== '' ? $e( $c['number'] ) : $e( 'Sipariş onaylandığında verilir' ), 'return_days'=>$withdraw === 14 ? '14 (on dört) gün' : $e( $withdraw ) . ' gün', 'return_days_note'=>$withdraw > 14 ? ' (satıcının sunduğu bu süre, kanuni 14 günlük süreden uzundur)' : '', 'dispatch_days'=>$e( (int) ( $s['dispatch_days'] ?: 3 ) ), 'return_carrier'=>$s['return_open'] === '1' ? $e( 'satıcının bildireceği kargo firması' ) : $carrier, 'return_address'=>$return_to, 'return_shipping'=>$s['return_open'] === '1' // No carrier named: the buyer pays nothing for the return, whichever way it travels (m.12/5). ? '

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):

' . ( $s['exceptions'] !== '' ? '

' . 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'=>'

Ek: Örnek cayma formu

(Bu form, yalnızca sözleşmeden cayma hakkı kullanılmak istendiğinde doldurulup gönderilir.)

', 'preinfo_title'=>esc_html( self::TITLES['preinfo'] ), 'signature'=>$c ? '

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 ); } }