{\rtf1\ansi\ansicpg1252\cocoartf2870 \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} {\*\expandedcolortbl;;} \paperw11900\paperh16840\margl1440\margr1440\vieww11520\viewh8400\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\fs24 \cf0 ['DYBO/01', 'One-page landing site', 199],\ 'dybo-05' => ['DYBO/05', 'Business website, up to 5 pages', 399],\ 'dybo-07' => ['DYBO/07', 'Website, up to 7 pages, two languages', 599],\ ];\ \ private static $statuses = [\ 'new' => 'New',\ 'invoiced' => 'Invoiced',\ 'paid' => 'Paid 50%',\ 'brief' => 'Brief received',\ 'live' => 'Live',\ 'cancelled' => 'Cancelled',\ ];\ \ public static function init() \{\ register_activation_hook(__FILE__, [__CLASS__, 'activate']);\ add_action('rest_api_init', [__CLASS__, 'routes']);\ add_action('admin_menu', [__CLASS__, 'menu']);\ add_action('admin_post_dybo_order_update', [__CLASS__, 'handle_update']);\ add_action('admin_post_dybo_order_delete', [__CLASS__, 'handle_delete']);\ add_action('admin_post_dybo_order_export', [__CLASS__, 'handle_export']);\ add_action('admin_post_dybo_order_settings', [__CLASS__, 'handle_settings']);\ \}\ \ public static function table() \{\ global $wpdb;\ return $wpdb->prefix . self::TABLE;\ \}\ \ public static function activate() \{\ global $wpdb;\ require_once ABSPATH . 'wp-admin/includes/upgrade.php';\ $charset = $wpdb->get_charset_collate();\ $sql = "CREATE TABLE " . self::table() . " (\ id bigint(20) unsigned NOT NULL AUTO_INCREMENT,\ created_at datetime NOT NULL,\ status varchar(20) NOT NULL DEFAULT 'new',\ plan varchar(20) NOT NULL,\ full_name varchar(190) NOT NULL,\ email varchar(190) NOT NULL,\ phone varchar(40) NOT NULL,\ business varchar(190) NOT NULL,\ website varchar(255) NOT NULL DEFAULT '',\ payer varchar(20) NOT NULL,\ company_name varchar(255) NOT NULL DEFAULT '',\ country char(2) NOT NULL DEFAULT '',\ address text NOT NULL,\ reg_number varchar(100) NOT NULL DEFAULT '',\ vat varchar(100) NOT NULL DEFAULT '',\ iban varchar(50) NOT NULL DEFAULT '',\ billing_email varchar(190) NOT NULL DEFAULT '',\ lang varchar(10) NOT NULL DEFAULT '',\ source varchar(255) NOT NULL DEFAULT '',\ notes text NOT NULL,\ PRIMARY KEY (id),\ KEY created_at (created_at),\ KEY status (status)\ ) $charset;";\ dbDelta($sql);\ update_option('dybo_order_db', self::VERSION);\ \}\ \ public static function settings() \{\ $defaults = [\ 'notify_to' => get_option('admin_email'),\ 'contact_email' => get_option('admin_email'),\ 'brief_url' => home_url('/project-brief/'),\ ];\ return wp_parse_args((array) get_option(self::OPT, []), $defaults);\ \}\ \ /* Front-end endpoint */\ \ public static function routes() \{\ register_rest_route('dybo/v1', '/order', [\ 'methods' => 'POST',\ 'callback' => [__CLASS__, 'submit'],\ 'permission_callback' => '__return_true',\ ]);\ \}\ \ public static function submit(WP_REST_Request $r) \{\ $p = $r->get_json_params();\ if (!is_array($p)) $p = [];\ $raw = function ($k) use ($p) \{\ return (isset($p[$k]) && is_scalar($p[$k])) ? trim((string) $p[$k]) : '';\ \};\ $txt = function ($k, $max = 190) use ($raw) \{\ return mb_substr(sanitize_text_field($raw($k)), 0, $max);\ \};\ \ // Spam protection\ if ($raw('company_website') !== '') return self::fake_ok();\ if ((int) $raw('elapsed') < 4000) \{\ return self::fail('Please take a moment to check your details, then send again.', [], 429);\ \}\ $origin = $r->get_header('origin');\ if ($origin && wp_parse_url($origin, PHP_URL_HOST) !== wp_parse_url(home_url(), PHP_URL_HOST)) \{\ return self::fail('Request not allowed.', [], 403);\ \}\ $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';\ $key = 'dybo_rl_' . md5($ip . wp_salt('nonce'));\ $count = (int) get_transient($key);\ if ($count >= 5) \{\ return self::fail('Too many attempts from this connection. Please try again in an hour, or email us at ' . self::settings()['contact_email'] . '.', [], 429);\ \}\ set_transient($key, $count + 1, HOUR_IN_SECONDS);\ \ $other_billing = $raw('other_billing') !== '';\ $d = [\ 'plan' => $txt('plan', 20),\ 'full_name' => $txt('full_name'),\ 'email' => sanitize_email($raw('email')),\ 'phone' => $txt('phone', 40),\ 'business' => $txt('business'),\ 'website' => esc_url_raw($raw('website')),\ 'payer' => $txt('payer', 20),\ 'company_name' => $txt('company_name', 255),\ 'country' => strtoupper($txt('country', 2)),\ 'address' => mb_substr(sanitize_textarea_field($raw('address')), 0, 1000),\ 'reg_number' => $txt('reg_number', 100),\ 'vat' => $txt('vat', 100),\ 'iban' => strtoupper(preg_replace('/\\s+/', '', $txt('iban', 60))),\ 'billing_email' => $other_billing ? sanitize_email($raw('billing_email')) : '',\ 'lang' => $txt('lang', 10),\ 'source' => esc_url_raw($raw('source')),\ ];\ \ $e = [];\ if (!isset(self::$plans[$d['plan']])) $e['plan'] = 'Please choose a plan.';\ if ($d['full_name'] === '') $e['full_name'] = 'Please enter your full name.';\ if (!is_email($d['email'])) $e['email'] = 'Please enter a valid email, we send your invoice there.';\ if (!preg_match('/^\\+[0-9][0-9 ()\\-]\{6,20\}$/', $d['phone'])) $e['phone'] = 'Please enter your number with the country code, for example +40 712 345 678.';\ if ($d['business'] === '') $e['business'] = 'Please enter your business or brand name.';\ if (!in_array($d['payer'], ['company', 'individual'], true)) $e['payer'] = 'Please choose Company or Individual.';\ if (!preg_match('/^[A-Z]\{2\}$/', $d['country'])) $e['country'] = 'Please choose your country.';\ if ($d['address'] === '') $e['address'] = 'Please enter the address for the invoice.';\ if ($d['payer'] === 'company') \{\ if ($d['company_name'] === '') $e['company_name'] = 'Please enter the full legal company name.';\ if ($d['reg_number'] === '') $e['reg_number'] = 'Please enter the company registration number.';\ \} else \{\ $d['company_name'] = $d['reg_number'] = $d['vat'] = '';\ \}\ if ($d['iban'] !== '' && !self::valid_iban($d['iban'])) $e['iban'] = 'This IBAN does not look right. Please check it or leave the field empty.';\ if ($other_billing && !is_email($d['billing_email'])) $e['billing_email'] = 'Please enter a valid billing email.';\ if ($raw('consent') === '') $e['consent'] = 'Please accept the Terms of Service to continue.';\ \ if ($e) return self::fail('Some details need your attention. Please check the fields marked in red.', $e, 422);\ \ global $wpdb;\ $row = array_merge($d, [\ 'created_at' => current_time('mysql', true),\ 'status' => 'new',\ 'notes' => '',\ ]);\ if (!$wpdb->insert(self::table(), $row)) \{\ return self::fail('We could not save your order. Please try again, or email us at ' . self::settings()['contact_email'] . '.', [], 500);\ \}\ $id = (int) $wpdb->insert_id;\ $ref = self::ref($id);\ self::send_emails($id, $d, $ref);\ \ return new WP_REST_Response(['ok' => true, 'ref' => $ref], 200);\ \}\ \ private static function fail($message, $fields = [], $code = 400) \{\ return new WP_REST_Response(['ok' => false, 'message' => $message, 'fields' => (object) $fields], $code);\ \}\ \ private static function fake_ok() \{\ return new WP_REST_Response(['ok' => true, 'ref' => 'DYBO-0000'], 200);\ \}\ \ public static function valid_iban($iban) \{\ if (!preg_match('/^[A-Z]\{2\}[0-9]\{2\}[A-Z0-9]\{10,30\}$/', $iban)) return false;\ $moved = substr($iban, 4) . substr($iban, 0, 4);\ $num = '';\ foreach (str_split($moved) as $c) \{\ $num .= ctype_alpha($c) ? (string) (ord($c) - 55) : $c;\ \}\ $mod = 0;\ foreach (str_split($num, 7) as $chunk) \{\ $mod = (int) ($mod . $chunk) % 97;\ \}\ return $mod === 1;\ \}\ \ /* Helpers */\ \ public static function ref($id) \{\ return 'DYBO-' . str_pad((string) $id, 4, '0', STR_PAD_LEFT);\ \}\ \ private static function when($gmt) \{\ return wp_date('d M Y, H:i', strtotime($gmt . ' UTC'));\ \}\ \ private static function plan_label($code) \{\ return isset(self::$plans[$code]) ? self::$plans[$code][0] . ', EUR ' . self::$plans[$code][2] : $code;\ \}\ \ private static function labels() \{\ return [\ 'plan' => 'Plan',\ 'full_name' => 'Full name',\ 'email' => 'Email',\ 'phone' => 'Phone',\ 'business' => 'Business',\ 'website' => 'Website or profile',\ 'payer' => 'Who is paying',\ 'company_name' => 'Company legal name',\ 'country' => 'Country',\ 'address' => 'Address',\ 'reg_number' => 'Registration number',\ 'vat' => 'VAT number',\ 'iban' => 'IBAN',\ 'billing_email' => 'Billing email',\ 'lang' => 'Form language',\ 'source' => 'Source page',\ ];\ \}\ \ private static function lines($d) \{\ $out = [];\ foreach (self::labels() as $k => $label) \{\ if (!isset($d[$k]) || $d[$k] === '') continue;\ $v = $k === 'plan' ? self::plan_label($d[$k]) : ($k === 'payer' ? ucfirst($d[$k]) : $d[$k]);\ $out[] = $label . ': ' . $v;\ \}\ return $out;\ \}\ \ private static function send_emails($id, $d, $ref) \{\ $set = self::settings();\ $plan = self::$plans[$d['plan']];\ \ $admin_body = "New DYBO order \{$ref\}\\n\\n" . implode("\\n", self::lines($d))\ . "\\n\\nOpen in admin: " . admin_url('admin.php?page=dybo-orders&order=' . $id);\ wp_mail(\ $set['notify_to'],\ "New DYBO order \{$ref\}: \{$d['business']\}, \{$plan[0]\}, " . ucfirst($d['payer']),\ $admin_body,\ ['Reply-To: ' . $d['full_name'] . ' <' . $d['email'] . '>']\ );\ \ $brief = add_query_arg(['plan' => $d['plan'], 'email' => $d['email'], 'ref' => $ref], $set['brief_url']);\ $half = number_format($plan[2] / 2, 2, '.', '');\ $invoice_to = $d['billing_email'] ? " to \{$d['billing_email']\}" : '';\ $client_body = "Hi \{$d['full_name']\},\\n\\n"\ . "Thank you for your DYBO order. Here is what happens next.\\n\\n"\ . "Order reference: \{$ref\}\\n"\ . "Plan: \{$plan[0]\}, \{$plan[1]\}, EUR \{$plan[2]\}\\n"\ . "First payment: EUR \{$half\}\\n\\n"\ . "1. We send your invoice for the first 50% shortly\{$invoice_to\}. Please mention \{$ref\} with your payment.\\n"\ . "2. Once it is paid, complete your project brief here:\\n\{$brief\}\\n"\ . "3. Your 72-hour clock starts when we have both the payment and the brief.\\n\\n"\ . "Questions? Just reply to this email.\\n\\n"\ . "The DYBO team";\ wp_mail($d['email'], "Your DYBO order \{$ref\} is in", $client_body, ['Reply-To: ' . $set['contact_email']]);\ \}\ \ /* Admin */\ \ public static function menu() \{\ global $wpdb;\ $new = (int) $wpdb->get_var("SELECT COUNT(*) FROM " . self::table() . " WHERE status = 'new'");\ $title = 'DYBO Orders' . ($new ? ' ' . $new . '' : '');\ add_menu_page('DYBO Orders', $title, 'manage_options', 'dybo-orders', [__CLASS__, 'page'], 'dashicons-cart', 26);\ add_submenu_page('dybo-orders', 'DYBO Orders', 'All orders', 'manage_options', 'dybo-orders', [__CLASS__, 'page']);\ add_submenu_page('dybo-orders', 'DYBO Settings', 'Settings', 'manage_options', 'dybo-orders-settings', [__CLASS__, 'settings_page']);\ \}\ \ private static function notice() \{\ $m = isset($_GET['dybo_msg']) ? sanitize_key($_GET['dybo_msg']) : '';\ $map = ['saved' => 'Order updated.', 'deleted' => 'Order deleted.', 'settings' => 'Settings saved.'];\ if (isset($map[$m])) \{\ echo '
' . esc_html($map[$m]) . '
| Ref | Received | Business | Contact | Plan | Payer | Country | Status | '\ . '
|---|---|---|---|---|---|---|---|
| No orders yet. | |||||||
| ' . esc_html(self::ref($o->id)) . ' | ';\ echo '' . esc_html(self::when($o->created_at)) . ' | ';\ echo '' . esc_html($o->business) . ' | ';\ echo '' . esc_html($o->full_name) . ' ' . esc_html($o->email) . ' ' . esc_html($o->phone) . ' | ';\
echo '' . esc_html(self::plan_label($o->plan)) . ' | ';\ echo '' . esc_html(ucfirst($o->payer)) . ' | ';\ echo '' . esc_html($o->country) . ' | ';\ echo '' . self::badge($o->status) . ' | ';\ echo '
| Received | ' . esc_html(self::when($o['created_at'])) . ' |
|---|---|
| Status | ' . self::badge($o['status']) . ' |
| ' . esc_html($label) . ' | ' . $html . ' |