فهرست منبع

Add current content from other repository

cixo 1 سال پیش
والد
کامیت
f08da88c1c
100فایلهای تغییر یافته به همراه5036 افزوده شده و 0 حذف شده
  1. 90 0
      activities/1-config_panel_activity.php
  2. 50 0
      activities/2-dashboard_activity.php
  3. 105 0
      activities/3-manage_messages_activity.php
  4. 178 0
      activities/4-edit_message_activity.php
  5. 297 0
      activities/5-manage_customers_activity.php
  6. 92 0
      activities/6-send_message_activity.php
  7. 73 0
      activities/7-show_campaigns_activity.php
  8. 133 0
      activities/8-import_customers_activity.php
  9. 108 0
      activities/9-manage_groups_activity.php
  10. 55 0
      builders/1-campaign_builder.php
  11. 52 0
      builders/2-response_builder.php
  12. 45 0
      compiler/make.sh
  13. 71 0
      components/01-plugin_updater.php
  14. 17 0
      components/02-message.php
  15. 28 0
      components/03-customer.php
  16. 29 0
      components/04-campaign.php
  17. 22 0
      components/05-send_log.php
  18. 23 0
      components/06-plugin_settings.php
  19. 86 0
      components/07-cron_emails_sender.php
  20. 11 0
      components/08-wordpress_dictionary.php
  21. 230 0
      components/09-plugin.php
  22. 32 0
      components/10-rest_register.php
  23. 20 0
      components/11-group.php
  24. 23 0
      converters/1-campaign_type_converter.php
  25. 31 0
      converters/2-messages_converter.php
  26. 62 0
      converters/3-customers-converter.php
  27. 64 0
      converters/4-campaigns_converter.php
  28. 29 0
      converters/5-group_converter.php
  29. 72 0
      cx-newsletter.php
  30. 26 0
      database/01-table_names.php
  31. 46 0
      database/02-database_versions_manager.php
  32. 132 0
      database/03-database_installer.php
  33. 105 0
      endpoints/1-messages_get_endpoint.php
  34. 115 0
      endpoints/2-messages_send_endpoint.php
  35. 26 0
      endpoints/3-settings_check_endpoint.php
  36. 8 0
      enums/1-campaign_type.php
  37. 9 0
      enums/2-toast_type.php
  38. 8 0
      enums/3-filter_type.php
  39. 8 0
      enums/4-filter_glue.php
  40. 11 0
      enums/5-response_code.php
  41. 9 0
      enums/6-response_status.php
  42. 9 0
      interfaces/01-database_builder_interface.php
  43. 10 0
      interfaces/02-database_converter_interface.php
  44. 9 0
      interfaces/03-database_item_interface.php
  45. 13 0
      interfaces/04-mapper_interface.php
  46. 10 0
      interfaces/05-validator_interface.php
  47. 10 0
      interfaces/06-enum_converter_interface.php
  48. 9 0
      interfaces/07-view_interface.php
  49. 9 0
      interfaces/08-settings_interface.php
  50. 8 0
      interfaces/09-builder_interface.php
  51. 8 0
      interfaces/10-service_interface.php
  52. 10 0
      interfaces/11-rest_endpoint_interface.php
  53. 9 0
      interfaces/12-rest_register_interface.php
  54. 7 0
      interfaces/13-plugin_worker_interface.php
  55. 1 0
      libs/CxAppengine
  56. 182 0
      mappers/1-messages_mapper.php
  57. 167 0
      mappers/2-customers_mapper.php
  58. 192 0
      mappers/3-groups_mapper.php
  59. 65 0
      pages/1-customers_export_page.php
  60. 11 0
      renders/add_new_customer.html
  61. 85 0
      renders/config_panel.html
  62. 101 0
      renders/customer_fields.html
  63. 32 0
      renders/dashboard.html
  64. 78 0
      renders/edit_message.html
  65. 72 0
      renders/edit_message.html.old
  66. 19 0
      renders/import_customers.html
  67. 112 0
      renders/manage_customers.html
  68. 42 0
      renders/manage_groups.html
  69. 27 0
      renders/manage_messages.html
  70. 71 0
      renders/manage_messages_single_message.html
  71. 7 0
      renders/only_toast_site.html
  72. 15 0
      renders/scripts/preview.js
  73. 19 0
      renders/send_message.html
  74. 7 0
      renders/show_campaigns.html
  75. 14 0
      renders/single_campaign.html
  76. 32 0
      renders/single_customer.html
  77. 39 0
      renders/single_group.html
  78. 1 0
      renders/single_group_option.html
  79. 10 0
      renders/styles/all.css
  80. 6 0
      renders/styles/colors.css
  81. 8 0
      renders/styles/column.css
  82. 13 0
      renders/styles/container.css
  83. 9 0
      renders/styles/font.css
  84. 17 0
      renders/styles/group.css
  85. 70 0
      renders/styles/inputs.css
  86. 8 0
      renders/styles/preview.css
  87. 13 0
      renders/styles/section.css
  88. 6 0
      renders/styles/separator.css
  89. 30 0
      renders/styles/toast.css
  90. 3 0
      renders/toast.html
  91. 132 0
      repositories/1-campaigns_repository.php
  92. 130 0
      repositories/2-send_log_repository.php
  93. 102 0
      templates/01-database_converter.php
  94. 90 0
      templates/02-database_worker.php
  95. 32 0
      templates/03-database_item.php
  96. 85 0
      templates/04-database_builder.php
  97. 9 0
      templates/05-mapper.php
  98. 23 0
      templates/06-table_names_generator.php
  99. 24 0
      templates/07-enum_converter.php
  100. 73 0
      templates/08-view.php

+ 90 - 0
activities/1-config_panel_activity.php

@@ -0,0 +1,90 @@
+<?php
+
+namespace cx_newsletter;
+
+class config_panel_activity
+extends activity {
+    public function show_after_button() : null {
+        return null;
+    }
+
+    public function inside_buttons() : array {
+        return [ 'save' ];
+    }
+
+    public function inside_inputs() : array {
+        return [
+            'source_address' => 'email',
+            'reply_to_address' => 'email',
+            'email_count' => 'int',
+            'sms_count' => 'int',
+            'apikey' => 'string'
+        ];          
+    }
+
+    public function get_site_template_name() : string {
+        return 'config_panel';
+    }
+
+    private function validate_result() : ?string { 
+        if (!$this->is_validated('source_address')) {
+            return 'Source E-Mail address is not valid.';
+        }   
+
+        if (!$this->is_validated('reply_to_address')) {
+            return 'Reply to E-Mail address it not valid.';
+        }
+
+        if (!$this->is_validated('email_count')) {
+            return 'Count of the E-Mails is not valid.';
+        }
+
+        if (!$this->is_validated('sms_count')) {
+            return 'Count of the SMS-es is not valid.';
+        }
+
+        if (!$this->is_validated('apikey')) {
+            return 'Api Key is not valid.';
+        }
+
+        if (strlen($this->get_validated('apikey')) < 16) {
+            return 'Api Key must has minimum 16 characters.';
+        }
+
+        return null;
+    }
+
+    private function save() { 
+        $validate = $this->validate_result();
+
+        if ($validate !== null) {
+            $this->error_toast($validate);
+            return;
+        }
+
+        $settings = $this->get_settings();
+        
+        $settings
+        ->save('source_address', $this->get_validated('source_address'))
+        ->save('reply_to_address', $this->get_validated('reply_to_address'))
+        ->save('email_count', $this->get_validated('email_count'))
+        ->save('sms_count', $this->get_validated('sms_count'))
+        ->save('apikey', $this->get_validated('apikey'));
+
+        $this->success_toast('Settings had been saved.');
+    }
+
+    public function process() : self {
+        if ($this->is_received('save')) {
+            $this->save();
+        }
+
+        $this->set('source_address', $this->get_settings()->get('source_address'));
+        $this->set('reply_to_address', $this->get_settings()->get('reply_to_address'));
+        $this->set('email_count', $this->get_settings()->get('email_count'));
+        $this->set('sms_count', $this->get_settings()->get('sms_count'));
+        $this->set('apikey', $this->get_settings()->get('apikey'));
+
+        return $this;
+    }
+}

+ 50 - 0
activities/2-dashboard_activity.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace cx_newsletter;
+
+class dashboard_activity
+extends activity {
+    public function show_after_button() : null {
+        return null;
+    }
+
+    public function get_site_template_name() : string {
+        return 'dashboard';
+    }
+
+    public function process() : self {
+        $messages_mapper = new messages_mapper(
+            $this->get_database(),
+            $this->get_tables()
+        );
+
+        $customers_mapper = new customers_mapper(
+            $this->get_database(),
+            $this->get_tables()
+        );
+
+        $this->set('messages_count', $messages_mapper->count());
+        $this->set('customers_count', $customers_mapper->count());
+
+        $messages_link = \menu_page_url(
+            'cx_newsletter_manage_messages', 
+            false
+        );
+        
+        $customers_link = \menu_page_url(
+            'cx_newsletter_manage_customers', 
+            false
+        );
+        
+        $campaigns_link = \menu_page_url(
+            'cx_newsletter_show_campaigns', 
+            false
+        );
+
+        $this->set('manage_messages_link', $messages_link);
+        $this->set('manage_customers_link', $customers_link);
+        $this->set('show_campaigns_link', $campaigns_link);
+
+        return $this;
+    }
+}

+ 105 - 0
activities/3-manage_messages_activity.php

@@ -0,0 +1,105 @@
+<?php
+
+namespace cx_newsletter;
+
+class manage_messages_activity
+extends activity {
+    public function show_after_button() : null {
+        return null;
+    }  
+
+    public function inside_buttons() : array {
+        return [ 'remove' ];
+    }
+
+    public function inside_inputs() : array {
+        return [
+            'id' => 'int'
+        ];
+    }
+
+    public function get_site_template_name() : string {
+        return 'manage_messages';
+    }
+
+    public function remove(messages_mapper $mapper) : void {
+        if (!$this->is_validated('id')) {
+            $this->error_toast('ID of the message is wrong.');
+        }
+
+        $message = new message($this->get_validated('id'));
+
+        try {
+            $mapper->remove($message);
+            $this->success_toast('Removed success.');
+        } catch (\exception $exception) {
+            $this->error_toast($exception->getMessage());
+        }
+    }
+
+    private function load_groups() : \cx_appengine\string_builder {
+        $mapper = new groups_mapper(
+            $this->get_database(),
+            $this->get_tables()
+        );
+
+        $default = new group();
+        $default->name = 'ALL';
+        $groups = $mapper->load_all();
+        $list = new \cx_appengine\string_builder();
+        array_push($groups, $default);
+
+        foreach ($groups as $group) {
+            $options = [];
+            $options['group_name'] = $group->name;
+
+            if ($group->has_id()) {
+                $options['group_id'] = $group->get_id();
+            } else {
+                $options['group_id'] = 'NULL';
+                $options['selected'] = 'selected';
+            }
+
+            $list->push(
+                $this
+                ->get_templates()
+                ->prepare('single_group_option')
+                ->render($options)
+            );
+        }
+
+        return $list;
+    }
+
+    public function process() : self {
+        $converter = new messages_converter();
+        $messages = new \cx_appengine\string_builder();
+        
+        $mapper = new messages_mapper(
+            $this->get_database(), 
+            $this->get_tables()
+        );
+
+        if ($this->is_received('remove')) {
+            $this->remove($mapper);
+        }
+       
+        $groups = $this->load_groups();
+
+        foreach ($mapper->load_all() as $message) {
+            $options = $converter->load_object($message)->get_array();
+            $options['groups'] = $groups;
+
+            $messages->push(
+                $this
+                ->get_templates()
+                ->prepare('manage_messages_single_message')
+                ->render($options)
+            );
+        }
+
+        $this->set('messages_place', $messages); 
+        
+        return $this;
+    }
+}

+ 178 - 0
activities/4-edit_message_activity.php

@@ -0,0 +1,178 @@
+<?php
+
+namespace cx_newsletter;
+
+class edit_message_activity 
+extends activity {
+    public function show_after_button() : string {
+        return 'create';
+    }
+
+    public function inside_buttons() : array {
+        return [ 'edit', 'save' ];
+    }
+
+    public function inside_inputs() : array {
+        return [
+            'id' => 'int',
+            'title' => 'string',
+            'content' => 'string'
+        ];
+    }
+
+    public function get_site_template_name() : string {
+        return 'edit_message';
+    }
+
+    public function prepare_edit() : void {
+        if (!$this->is_validated('id')) {
+            $this->error_toast('ID of the message to edit is wrong.');
+            return;
+        }
+        
+        $mapper = new messages_mapper(
+            $this->get_database(),
+            $this->get_tables()
+        );
+
+        $message = $mapper->complete(new message($this->get_validated('id')));
+
+        $this->set('id', $message->get_id());
+        $this->set('title', $message->title);
+        $this->set('content', $message->content);
+    }
+
+    public function create() : void {
+        $message = new message();
+        $message->title = $this->get_validated('title');
+        $message->content = stripslashes($this->get_validated('content'));
+
+        $mapper = new messages_mapper(
+            $this->get_database(),
+            $this->get_tables()
+        );
+
+        try {
+            $message = $mapper->create($message);
+            $this->success_toast('Created successfull.');
+
+            $this->set('id', $message->get_id());
+            $this->set('title', $message->title);
+            $this->set('content', $message->content);
+        } catch (\exception $exception) {
+            $this->error_toast($exception->getMessage());
+        }
+    }
+
+    public function own_validation() : ?string {
+        if (!$this->is_validated('title')) {
+            return 'Title is not sended.';
+        }
+
+        if (!$this->is_validated('content')) {
+            return 'Content is not sended.';
+        }
+
+        $title = $this->get_validated('title');
+        $content = $this->get_validated('content');
+
+        if ($this->is_validated('id')) {
+            $this->set('id', $this->get_validated('id'));
+        }
+
+        if (strlen($title) < 8) {
+            return 'Title must be longer than 8 characters.';
+        }
+
+        if (\esc_html($title) !== $title) {
+            return 'Title can not has HTML characters.';
+        }
+        
+        return null;
+    }
+
+    public function save() : void {
+        $result = $this->own_validation();
+
+        if ($result !== null) {
+            $this->warning_toast($result);
+            return;
+        }
+
+        if (!$this->is_validated('id')) {
+            $this->create();
+            return;
+        }
+
+        $mapper = new messages_mapper(
+            $this->get_database(),
+            $this->get_tables()
+        );
+
+        $message = new message($this->get_validated('id'));
+        $message->title = $this->get_validated('title');
+        $message->content = stripslashes($this->get_validated('content'));
+
+        try {
+            $mapper->save($message);
+            $this->success_toast('Message saved.');
+
+            $this->set('id', $message->get_id());
+            $this->set('title', $message->title);
+            $this->set('content', $message->content);
+        } catch (\exception $exception) {
+            $this->error_toast($exception->getMessage());
+        }
+    }
+
+    public function process() : self {
+        if ($this->is_validated('title')) {
+            $this->set('title', $this->get_validated('title'));
+        }
+
+        if ($this->is_validated('content')) {
+            $this->set(
+                'content', 
+                stripslashes($this->get_validated('content'))
+            );
+        }
+        
+        if ($this->is_validated('id')) {
+            $this->set('id', $this->get_validated('id'));
+        }
+        
+        if ($this->is_received('save')) {
+            $this->save();
+        }
+
+        if ($this->is_received('edit')) {
+            $this->prepare_edit();
+        }
+
+        $content = '';
+
+        if ($this->get('content') !== null) {
+            $content = $this->get('content');
+        }
+
+        \ob_start();
+        \wp_editor($content, 'content', [
+            'textarea_rows' => '',
+            'quicktags' => false,
+            'wpautoup' => false,
+            'media_buttons' => true,
+            'textarea_name' => 'content',
+            'teeny' => true,
+            'tinymce' => [
+                'content_css' => false,
+                'inline' => false
+            ]
+        ]);
+
+        $result = \ob_get_clean();
+
+        $this->set('editor', $result);
+
+        return $this;
+    }
+}

+ 297 - 0
activities/5-manage_customers_activity.php

@@ -0,0 +1,297 @@
+<?php
+
+namespace cx_newsletter;
+
+class manage_customers_activity
+extends activity {
+    public function show_after_button() : null {
+        return null;
+    }
+
+    public function inside_buttons() : array {
+        return [ 'add', 'save', 'remove', 'search' ];
+    }
+    
+    public function inside_inputs() : array {
+        return [
+            'company' => 'string',
+            'name' => 'string',
+            'surname' => 'string',
+            'comments' => 'string ',
+            'email' => 'email',
+            'phone_number' => 'phone',
+            'tester' => 'bool',
+            'limit' => 'int',
+            'filter' => 'string',
+            'id' => 'int',
+            'group' => 'custom',
+        ];
+    }
+
+    public function get_site_template_name() : string {
+        return 'manage_customers';
+    }
+
+    private function get_all_groups() : array {
+
+    }
+
+    private function get_limit() : int {
+        if ($this->is_validated('limit')) {
+            $this->set('limit', $this->get_validated('limit'));
+            return $this->get_validated('limit');
+        }
+
+        $this->warning_toast('Limit is not correct, it must be number.');
+        return 50;
+    }
+
+    private function get_filter() : string {
+        if ($this->is_validated('filter')) {
+            $this->set('filter', $this->get_validated('filter'));
+            return $this->get_validated('filter');
+        }
+
+        $this->warning_toast('Filter value is not correct.');
+        return '';
+    }
+
+    private function prepare_groups(?group $target = null) : string {
+        $render = new \cx_appengine\string_builder();
+
+        if ($target !== null) {
+            $target = $target->get_id();
+        }
+
+        foreach ($this->groups as $group) {
+            $options = [];
+            
+            if ($group->has_id()) {
+                $options['group_id'] = $group->get_id();
+            } else {
+                $options['group_id'] = 'NULL';
+            }
+
+            $selected = false;
+            $group_id = null;
+
+            if ($group->has_id()) {
+                $group_id = $group->get_id();
+            }
+
+            if ($target === $group_id) {
+                $selected = true;
+            }
+
+            if ($selected) {
+                $options['selected'] = 'selected';
+            }
+
+            $options['group_name'] = $group->name;
+
+            $render->push(
+                $this
+                ->get_templates()
+                ->prepare('single_group_option')
+                ->render($options)
+            );
+        }
+
+        return $render->get();
+    }
+
+    private function load_customers() : \cx_appengine\string_builder {
+        $limit = 50;
+        $filter = '';
+        $list = new \cx_appengine\string_builder();
+
+        if ($this->is_received('search')) {
+            $limit = $this->get_limit();
+            $filter = $this->get_filter();
+        }
+        
+        try {
+            $customers = $this->mapper->find($filter, $limit);
+            $converter = new customers_converter();
+
+            foreach ($customers as $customer) {
+                $options = $converter->load_object($customer)->get_array();
+                
+                if ($customer->tester) {
+                    $options['tester'] = 'checked';
+                }
+
+                $options['groups'] = $this->prepare_groups($customer->group);
+
+                $list->push(
+                    $this
+                    ->get_templates()
+                    ->prepare('single_customer')
+                    ->render($options)
+                );
+            }
+        } catch (\exception $exception) {
+            $this->error_toast('Can not list. '.$exception->getMessage());
+        } finally {
+            return $list;
+        }
+    }
+
+    public function preprocess() : void {
+        $this->mapper = new customers_mapper(
+            $this->get_database(), 
+            $this->get_tables()
+        );
+
+        $this->groups_mapper = new groups_mapper(
+            $this->get_database(),
+            $this->get_tables()
+        );
+
+        $this->groups = $this->groups_mapper->load_all();
+
+        $nogroup = new group();
+        $nogroup->name = 'nogroup';
+
+        array_push($this->groups, $nogroup);
+    }
+
+    private function check_validation(array $list) : bool {
+        foreach ($list as $item) {
+            if (!$this->is_validated($item)) {
+                $this->error_toast(
+                    $item.__(' is not correct.', 'cx_newsletter')
+                );
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    private function get_customer_from_inputs() : ?customer {
+        $id = null;
+
+        if ($this->is_validated('id')) {
+            $id = $this->get_validated('id');
+        }
+
+        $list = [
+            'name',
+            'surname',
+            'comments',
+            'company',
+        ];
+
+        if (!$this->check_validation($list)) {
+            return null;
+        }
+
+        $customer = new customer($id);
+        $customer->name = $this->get_validated('name');
+        $customer->surname = $this->get_validated('surname');
+        $customer->company = $this->get_validated('company');
+        $customer->comments = $this->get_validated('comments');
+        $customer->email = $this->get_received('email', '');
+        $customer->phone_number = $this->get_received('phone_number', '');
+        $customer->tester = $this->is_received('tester');
+
+        $group = $this->get_received('group');
+
+        if ($group === 'NULL') {
+            $customer->group = null;
+        } else {
+            $customer->group = new group(intval($group));
+        }
+
+        $validator = new customer_validator(
+            $this->get_database(), 
+            $this->get_tables()
+        );
+
+        $validator
+        ->load($customer)
+        ->check();
+
+        if (!$validator->is_valid()) {
+            $this->error_toast($validator->error_on());
+            return null;
+        }
+
+        return $customer;
+    }
+
+    public function add_customer() : void {
+        $customer = $this->get_customer_from_inputs();   
+
+        if ($customer === null) {
+            return;
+        }   
+
+        try {
+            $this->mapper->create($customer);
+            $this->success_toast('Add customer success.');
+        } catch (\exception $exception) {
+            $this->error_toast($exception->getMessage());
+        }
+    }
+
+    public function save_customer() : void {
+        $customer = $this->get_customer_from_inputs();
+        
+        if ($customer === null) {
+            return;
+        }
+
+        try {
+            $this->mapper->save($customer);
+            $this->success_toast('Save customer success.');
+        } catch (\exception $exception) {
+            $this->error_toast($exception->getMessage());
+        }
+    }
+
+    public function remove_customer() : void {
+        $customer = $this->get_customer_from_inputs();
+        
+        if ($customer === null) {
+            return;
+        }
+
+        try {
+            $this->mapper->remove($customer);
+            $this->success_toast('Remove customer success.');
+        } catch (\exception $exception) {
+            $this->error_toast($exception->getMessage());
+        }
+    }
+
+    public function process() : self {
+        $this->preprocess();
+
+        if ($this->is_received('add')) {
+            $this->add_customer();
+        }
+
+        if ($this->is_received('save')) {
+            $this->save_customer();
+        }
+
+        if ($this->is_received('remove')) {
+            $this->remove_customer();
+        }       
+       
+        $this->set('groups', $this->prepare_groups());
+        $this->set('all_customers', $this->load_customers());
+        $this->set(
+            'export', 
+            \get_home_url().'/cx-newsletter/customers-export'
+        );
+
+        return $this;
+    }
+
+    private array $groups;
+    private groups_mapper $groups_mapper;
+    private customers_mapper $mapper;
+}

+ 92 - 0
activities/6-send_message_activity.php

@@ -0,0 +1,92 @@
+<?php
+
+namespace cx_newsletter;
+
+class send_message_activity
+extends activity {
+public function show_after_button() : string {
+        return 'send_via_email';
+    }
+
+    public function inside_buttons() : array {
+        return [ 'send_via_sms', 'test_via_email', 'test_via_sms' ];
+    }
+
+    public function init_inputs() : array {
+        return [
+            'id' => 'int'
+        ];
+    }
+
+    public function inside_inputs() : array {
+        return $this->init_inputs();
+    }
+
+    public function get_site_template_name() : string {
+        return 'send_message';
+    }
+
+    public function process() : self {
+        if (!$this->is_validated('id')) {
+            $this->error_toast('Message ID is not valid.');
+        }
+
+        try {
+            $mapper = new messages_mapper(
+                $this->get_database(), 
+                $this->get_tables()
+            );
+
+            $message = new message($this->get_validated('id'));
+            $message = $mapper->complete($message);
+
+            $builder = new campaign_builder();
+            
+            $builder->set_test();
+            
+            if ($this->is_received('send_via_sms')) {
+                $builder->set_test(false);
+            }
+
+            if ($this->is_received('send_via_email')) {
+                $builder->set_test(false);
+            }
+
+            $builder->set_type(campaign_type::email);
+
+            if ($this->is_received('send_via_sms')) {
+                $builder->set_type(campaign_type::sms);
+            }
+
+            if ($this->is_received('test_via_sms')) {
+                $builder->set_type(campaign_type::sms);
+            }
+
+            $builder->set_message($message);
+
+            if ($this->get_received('group') !== 'NULL') {
+                $group = new group(intval($this->get_received('group')));
+                $builder->set_group($group);
+            }
+
+            if (!$builder->is_complete()) {
+                throw new \exception('Builder is yet not complete.');
+            }
+
+            $campaign = $builder->get();
+
+            $repository = new campaigns_repository(
+                $this->get_database(),
+                $this->get_tables()
+            );      
+
+            $repository->create($campaign);
+
+            $this->success_toast('Start sending it...');
+        } catch (\exception $exception) {
+            $this->error_toast($exception->getMessage());
+        }
+
+        return $this;
+    }
+}

+ 73 - 0
activities/7-show_campaigns_activity.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace cx_newsletter;
+
+class show_campaigns_activity 
+extends activity {
+    public function show_after_button() : null {
+        return null;
+    }   
+
+    public function get_site_template_name() : string {
+        return 'show_campaigns';
+    }
+
+    public function load_campaigns() : \cx_appengine\string_builder {
+        $result = new \cx_appengine\string_builder();
+
+        $repository = new campaigns_repository(
+            $this->get_database(),
+            $this->get_tables()
+        );
+
+        $sended = new send_log_repository(
+            $this->get_database(),
+            $this->get_tables()
+        );
+
+        $template = (
+            $this
+            ->get_templates()
+            ->prepare('single_campaign')
+        );
+
+        foreach ($repository->load_all() as $campaign) {
+            $options = [];
+            $options['id'] = $campaign->get_id();
+            $options['title'] = $campaign->message->title;
+            $options['sended'] = $sended->count($campaign, true);
+
+            if ($campaign->finalized === null) {
+                $options['to_send'] = $sended->count($campaign, false);
+            } else {
+                $options['to_send'] = 0;
+            }
+
+            $options['finalized'] = $campaign->finalized !== null ?
+                $campaign->finalized->format('Y-m-d H:i:s') : 
+                __('No', 'cx_newsletter');
+
+            $options['test'] = $campaign->test ? 
+                __('Yes', 'cx_newsletter') : 
+                __('No', 'cx_newsletter');
+
+            $options['type'] = match ($campaign->type) {
+                campaign_type::sms => 
+                    __('SMS campaign', 'cx_newsletter'),
+                
+                campaign_type::email =>
+                    __('E-Mail campaign', 'cx_newsletter')
+            };
+
+            $result->push($template->render($options));
+        }
+
+        return $result;
+    }
+
+    public function process() : self {
+        $this->set('campaigns', $this->load_campaigns());
+
+        return $this;
+    }
+}

+ 133 - 0
activities/8-import_customers_activity.php

@@ -0,0 +1,133 @@
+<?php
+
+namespace cx_newsletter;
+
+class import_customers_activity
+extends activity {
+    public function show_after_button() : string {
+        return 'import';
+    }
+    
+    public function get_site_template_name() : string {
+        return 'import_customers';
+    }
+
+    public function inside_buttons() : array {
+        return [ 'clean' ];
+    }
+
+    public function process() : self {
+        if ($this->is_received('import')) {
+            return $this->import();
+        } elseif($this->is_received('clean')) {
+            return $this->clean();
+        }
+    }
+
+    public function clean() : self {
+        $mapper = new customers_mapper(
+            $this->get_database(),
+            $this->get_tables()
+        );
+
+        $customers = $mapper->load_all();
+
+        try {
+            foreach ($customers as $customer) {
+                $mapper->remove($customer);
+            }
+            
+            $this->success_toast('Customers database cleaned success.');
+        } catch (\exception $exception) {
+            $this->error_toast($exception->getMessage());
+        }   
+
+        return $this;
+    }
+
+    public function import() : self {
+        if (!isset($_FILES['upload'])) {
+            $this->error_toast('Input with file not exists.');
+            return $this;
+        }
+        
+        if (!is_uploaded_file($_FILES['upload']['tmp_name'])) {
+            $this->error_toast('Not upload file that would be imported.');
+            return $this;
+        }
+
+        $coded = file_get_contents($_FILES['upload']['tmp_name']);
+        $decoded = json_decode($coded, true);
+        
+        $group = null;
+        
+        if ($this->get_received('group') !== 'NULL') {
+            $group = $this->get_received('group');
+        }
+
+        if ($decoded === null) {
+            $this->error_toast('Uploaded JSON is not valid.');
+            return $this;
+        }
+
+        ini_set('max_execution_time', '360');
+
+        $mapper = new customers_mapper(
+            $this->get_database(), 
+            $this->get_tables()
+        );
+        
+        $validator = new customer_validator(
+            $this->get_database(),
+            $this->get_tables()
+        );
+
+        $converter = new customers_converter();
+
+        $added = 0;
+        $failed = 0;
+
+        foreach ($decoded as $item) {
+            $item['grouped'] = $group;
+
+            try {
+                $customer = $converter->load_array($item)->get_object();
+                $validator->load($customer)->check();
+
+                if (!$validator->is_valid()) {
+                    $failed += 1;
+                    continue;
+                }   
+            } catch (\exception $exception) {
+                $error = new \cx_appengine\string_builder();
+                $error->push(
+                    __('Customers file is corrupted.', 'cx_newsletter').' '
+                );
+                $error->push($exception->getMessage());
+
+                $this->error_toast($error->get());
+                $failed += 1;
+                continue;
+            }
+            
+            try {
+                $mapper->create($customer);
+                $added += 1;
+            } catch (\exception $exception) {
+                $this->error_toast($exception->getMessage());
+                $failed += 1;
+                continue;
+            }
+        }
+        
+        $result = new \cx_appengine\string_builder();
+        $result->push(__('Import new customers success.', 'cx_newsletter'));
+        $result->push('<br>');
+        $result->push(__('Added: ', 'cx_newsletter').$added.'<br>');
+        $result->push(__('Failed: ', 'cx_newsletter').$failed);
+
+        $this->success_toast($result->get());
+
+        return $this;
+    }
+}

+ 108 - 0
activities/9-manage_groups_activity.php

@@ -0,0 +1,108 @@
+<?php
+
+namespace cx_newsletter;
+use \cx_appengine\string_builder;
+use \exception;
+
+class manage_groups_activity
+extends activity {
+    public function show_after_button() : null {
+        return null;
+    }
+
+    public function get_site_template_name() : string {
+        return 'manage_groups';
+    }
+
+    public function inside_buttons() : array {
+        return [ 'delete', 'save', 'add' ];
+    }
+
+    public function inside_inputs() : array {
+        return [
+            'id' => 'int',
+            'name' => 'string'
+        ];
+    }
+
+    public function process() : self {
+        if ($this->is_received('save')) {
+            $this->save();    
+        }
+
+        if ($this->is_received('add')) {
+            $this->add();
+        }
+
+        if ($this->is_received('delete')) {
+            $this->delete();
+        }
+
+        $this->set('groups', $this->load_groups());
+        
+        return $this;
+    }
+
+    private function save() : void {
+        try {
+            $group = new group($this->get_validated('id'));
+            $group->prepare_name($this->get_validated('name'));
+            $this->get_mapper()->save($group);
+        } catch (exception $exception) {
+            $error = 'An error exception when saving group. More info: ';
+            $error .= $exception->getMessage();
+
+            $this->error_toast($error);
+        }
+    }   
+
+    private function add() : void {
+        try {
+            $group = new group();
+            $group->prepare_name($this->get_validated('name'));
+    
+            $this->get_mapper()->create($group);
+        } catch (exception $exception) {
+            $error = 'An error exception when saving group. More info: ';
+            $error .= $exception->getMessage();
+
+            $this->error_toast($error);
+        }
+    }
+
+    private function delete() : void {
+        try {   
+            $group = new group($this->get_validated('id'));
+
+            $this->get_mapper()->remove($group);
+        } catch (exception $exception) {
+            $error = 'An error exception when removing group. More info: ';
+            $error .= $exception->getMessage();
+
+            $this->error_toast($error);
+        }
+    }
+
+    private function load_groups() : string_builder {
+        $converter = new group_converter();        
+        $groups = new string_builder();
+
+        foreach ($this->get_mapper()->load_all() as $group) {
+            $groups->push(
+                $this
+                ->get_templates()
+                ->prepare('single_group')
+                ->render($converter->load_object($group)->get_array())
+            );
+        }
+
+        return $groups;
+    }
+
+    private function get_mapper() : groups_mapper {
+        return new groups_mapper(
+            $this->get_database(),
+            $this->get_tables()
+        );
+    }
+}

+ 55 - 0
builders/1-campaign_builder.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace cx_newsletter;
+
+class campaign_builder
+extends builder
+implements builder_interface {
+    protected function prepare() : void {
+        $this->target = new campaign();
+    }
+
+    public function set_type(campaign_type $type) : self {
+        $this->target->type = $type;
+
+        if ($this->target->is_complete()) {
+            $this->complete();
+        }
+
+        return $this;
+    }
+
+    public function set_message(message $message) : self {
+        if (!$message->has_id()) {
+            throw new \exception('Message to set for campaign must has ID.');
+        }
+
+        $this->target->message = $message;
+
+        if ($this->target->is_complete()) {
+            $this->complete();
+        }
+
+        return $this;
+    }
+
+    public function set_group(group $group) : self {
+        $this->target->group = $group;
+
+        if ($this->target->is_complete()) {
+            $this->complete();
+        }
+
+        return $this;
+    }
+
+    public function set_test(bool $test = true) : self {
+        $this->target->test = $test;
+
+        if ($this->target->is_complete()) {
+            $this->complete();
+        }
+    
+        return $this;
+    }
+}

+ 52 - 0
builders/2-response_builder.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace cx_newsletter;
+
+class response_builder {
+    public function __construct() {
+        $this->code = response_code::SUCCESS;
+        $this->status = response_status::SUCCESS;
+        $this->message = '';
+        $this->content = [];
+    }
+
+    public function set_code(response_code $code) : self {
+        $this->code = $code;
+        
+        return $this;
+    }
+
+    public function set_status(response_status $status) : self {
+        $this->status = $status;
+    
+        return $this;
+    }
+
+    public function set_message(string $message) : self {
+        $this->message = $message;
+
+        return $this;
+    }
+
+    public function set_content(array|string $content) : self {
+        $this->content = $content;
+
+        return $this;
+    }
+
+    public function build() : array {
+        return [
+            'Result' => [
+                'Status' => $this->status->value,
+                'Message' => $this->message,
+                'Code' => $this->code->value
+            ],
+            'Content' => $this->content
+        ];
+    }
+
+    private string $message;
+    private array|string $content;
+    private response_code $code;
+    private response_status $status;
+}

+ 45 - 0
compiler/make.sh

@@ -0,0 +1,45 @@
+#!/bin/bash
+
+# Plugin resuorces
+dirs=( \
+    activities \
+    converters \
+    database \
+    enums \
+    libs \
+    pages \
+    repositories \
+    traits \
+    validators \
+    builders \
+    components \
+    cx-newsletter.php \
+    endpoints \
+    interfaces \
+    mappers \
+    renders \
+    templates \
+    translates \
+    views \
+)
+
+# Change to compile directory
+cd $(dirname $(realpath $0))
+
+# Remove old build if exists
+rm ./cx-newsletter.zip -f
+rm ./cx-newsletter/ -rf
+
+# Create build directory
+mkdir ./cx-newsletter
+
+# Copy content to build directory
+for dir in ${dirs[@]}; do
+    cp ../$dir ./cx-newsletter/ -r
+done
+
+# Build plugin zip
+zip cx-newsletter.zip cx-newsletter/ -r 2> /dev/null > /dev/null
+
+# Remove copy of the resouces after build
+rm ./cx-newsletter/ -rf 

+ 71 - 0
components/01-plugin_updater.php

@@ -0,0 +1,71 @@
+<?php
+
+namespace cx_newsletter;
+
+class plugin_updater 
+extends plugin_worker
+implements plugin_worker_interface {
+    public function update() : void { 
+        $init_settings = !(
+            $this
+            ->get_settings()
+            ->is_initializated()
+        );
+
+        $init_database = !(
+            $this
+            ->get_versions()
+            ->is_installed()
+        );
+
+        if ($init_settings or $init_database) {
+            $this
+            ->install();
+        }
+
+
+        $this
+        ->get_installer()
+        ->update();
+    }
+
+    public function install() : void {
+        $this
+        ->remove();
+        
+        $this
+        ->get_settings()
+        ->init();
+        
+        $this
+        ->get_installer()
+        ->init()
+        ->update();
+    }
+
+    public function remove() : void {
+        $this
+        ->get_settings()
+        ->clean();
+
+        $this
+        ->get_installer()
+        ->clean();
+    }
+
+    private function get_installer() : database_installer {
+        if ($this->installer !== null) {
+            return $this->installer;
+        }   
+
+        $this->installer = new database_installer(
+            $this->get_database(),
+            $this->get_tables(),
+            $this->get_versions()
+        );
+
+        return $this->get_installer();
+    }
+
+    private ?database_installer $installer = null;
+}

+ 17 - 0
components/02-message.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace cx_newsletter;
+
+class message
+extends database_item
+implements database_item_interface {
+    public ?string $title = null;
+    public ?string $content = null;
+
+    public function is_complete() : bool {
+        if ($this->title === null) return false;
+        if ($this->content === null) return false;
+
+        return true;
+    }
+}   

+ 28 - 0
components/03-customer.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace cx_newsletter;
+
+class customer 
+extends database_item
+implements database_item_interface {
+    public ?string $company = null;
+    public ?string $name = null;
+    public ?string $surname = null;
+    public ?string $comments = null;
+    public ?string $email = null;
+    public ?string $phone_number = null;
+    public ?bool $tester = null;
+    public ?group $group = null;
+
+    public function is_complete() : bool {
+        if ($this->company === null) return false;
+        if ($this->name === null) return false;
+        if ($this->surname === null) return false;
+        if ($this->comments === null) return false;
+        if ($this->email === null) return false;
+        if ($this->phone_number === null) return false;
+        if ($this->tester === null) return false;
+
+        return true;
+    }
+}

+ 29 - 0
components/04-campaign.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace cx_newsletter;
+
+class campaign 
+extends database_item
+implements database_item_interface {
+    public ?message $message = null;
+    public ?campaign_type $type = null;
+    public ?bool $test = null;
+    public ?\datetime $finalized = null;
+    public ?group $group = null;
+
+    public function finalize() : self {
+        $this->finalized = new \datetime();
+        return $this;
+    }
+
+    public function is_complete() : bool {
+        if ($this->message === null) return false;
+        if ($this->type === null) return false;
+        if ($this->test === null) return false;
+
+        if (!$this->message->is_complete()) return false;
+        if (!$this->message->has_id()) return false;
+
+        return true;
+    }
+}

+ 22 - 0
components/05-send_log.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace cx_newsletter;
+
+class send_log
+extends database_item
+implements database_item_interface {
+    public ?campaign $campaign = null;
+    public ?customer $customer = null;
+    public ?\datetime $sended = null;
+
+    public function is_complete() : bool {
+        if ($this->campaign === null) return false;
+        if ($this->customer === null) return false;
+        if ($this->sended === null) return false;
+
+        if (!$this->campaign->is_complete()) return false;
+        if (!$this->customer->is_complete()) return false;
+    
+        return true;
+    }
+}

+ 23 - 0
components/06-plugin_settings.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace cx_newsletter;
+use \exception;
+
+class plugin_settings
+extends settings
+implements settings_interface {
+    protected function get_options() : array {
+        return [
+            'source_address' => '[email protected]',
+            'reply_to_address' => '[email protected]',
+            'email_count' => '50',
+            'sms_count' => '10',
+            'database_version' => 'null',
+            'apikey' => 'TEST_APIKEY',
+        ];
+    }
+
+    public function get_plugin_version() : int {
+        return 2;
+    }   
+}

+ 86 - 0
components/07-cron_emails_sender.php

@@ -0,0 +1,86 @@
+<?php
+
+namespace cx_newsletter;
+
+class cron_emails_sender
+extends service
+implements service_interface {
+    public function prepare() : self {
+        $this->campaigns_repository = new campaigns_repository(
+            $this->get_database(),
+            $this->get_tables()
+        );
+
+        $this->send_log_repository = new send_log_repository(
+            $this->get_database(),
+            $this->get_tables()
+        );
+        
+        $this->customers_mapper = new customers_mapper(
+            $this->get_database(),
+            $this->get_tables()
+        );
+
+        $this->messages_mapper = new messages_mapper(
+            $this->get_database(),
+            $this->get_tables()
+        );
+        
+        return $this;
+    }
+
+    public function process() : self {
+        $next_campaign = $this->campaigns_repository->load_next(
+            campaign_type::email
+        );
+
+        if ($next_campaign === null) {
+            return $this;
+        }
+
+        $customers = $this->send_log_repository->load_all(
+            $next_campaign,
+            $this->get_settings()->get('email_count'),
+            false
+        );
+
+        if (empty($customers)) {
+            $next_campaign->finalize();
+            $this->campaigns_repository->save($next_campaign);
+
+            return $this->process();
+        }
+
+        foreach ($customers as $customer) {
+            if ($this->send($customer, $next_campaign->message)) {
+                $this->send_log_repository->mark_send($next_campaign, $customer);
+            }
+        }
+
+        return $this;
+    }
+
+    private function send(customer $to, message $what) : bool {
+        if (empty($to->email)) {
+            return false;
+        }   
+
+        if (!$what->is_complete()) {
+            return false;
+        }
+
+        $headers = [];
+        $headers['From'] = $this->get_settings()->get('source_address');
+        $headers['Reply-To'] = $this->get_settings()->get('reply_to_address');
+        $headers['MIME-Version'] = '1.0';
+        $headers['Content-Type'] = 'text/html';
+        
+        return \wp_mail($to->email, $what->title, $what->content, $headers);
+    }
+
+    private campaigns_repository $campaigns_repository;
+    private send_log_repository $send_log_repository;
+    private customers_mapper $customers_mapper;
+    private messages_mapper $messages_mapper;
+    private campaign $what;
+}

+ 11 - 0
components/08-wordpress_dictionary.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace cx_newsletter;
+
+class wordpress_dictionary extends \cx_appengine\dictionary {
+    public function translate(
+        \cx_appengine\string_builder $content
+    ) : \cx_appengine\string_builder {
+        return new \cx_appengine\string_builder(__($content, 'cx_newsletter'));
+    }
+}

+ 230 - 0
components/09-plugin.php

@@ -0,0 +1,230 @@
+<?php
+
+namespace cx_newsletter;
+use \add_action;
+use \add_filter;
+use \load_plugin_textdomain;
+use \wp_next_scheduled;
+use \wp_schedule_event;
+use \add_menu_page;
+use \wpdb;
+
+class plugin {
+    public function __construct(string $plugin) {
+        add_action('admin_menu', [$this, 'admin']);
+        add_action('init', [$this, 'init']);
+        add_action('rest_api_init', [$this, 'register_rest']);
+        add_action('cx_newsletter_email_sender', [$this, 'email_sender']);
+        add_filter('cron_schedules', [$this, 'add_schedules']);
+   
+        global $wpdb;
+        $this->database = $wpdb;
+        $this->plugin = $plugin;
+
+        $directory = new \cx_appengine\directory(templates_dir, 'html');
+        $dictionary = new wordpress_dictionary;
+        $this->templates = new \cx_appengine\templates($directory, $dictionary);
+        $this->settings = new plugin_settings();
+        $this->tables = new table_names($this->get_database());
+        $this->versions = new database_versions_manager();
+
+        $this->init_cron();
+    }
+
+    public function init() { 
+        $this->export = new customers_export_page(
+            $this->database,
+            $this->tables,
+            $this->settings
+        );
+
+        $this->export->prepare();
+
+        $this->translations();
+    }
+    
+    private function translations() : void { 
+        load_plugin_textdomain(
+            'cx_newsletter', 
+            false, 
+            $this->plugin.'/translates');
+    }
+
+    public function add_schedules(array $intervals) : array {
+        $intervals['cx_newsletter_send'] = [];
+        $intervals['cx_newsletter_send']['interval'] = 60;
+        $intervals['cx_newsletter_send']['display'] = 'cx-newsletter';
+
+        return $intervals;
+    }
+
+    public function register_rest() : void {
+        $this->messages_get = new messages_get_endpoint(
+            $this->database,
+            $this->settings,
+            $this->tables
+        );
+
+        $this->messages_send = new messages_send_endpoint(
+            $this->database,
+            $this->settings,
+            $this->tables
+        );
+
+        $this->settings_check = new settings_check_endpoint(
+            $this->database,
+            $this->settings,
+            $this->tables
+        );
+
+        $registerer = new rest_register('cx-newsletter/v1');
+        $registerer
+        ->register($this->messages_get)
+        ->register($this->messages_send)
+        ->register($this->settings_check);
+    }
+
+    private function init_cron() : void {
+        if (wp_next_scheduled('cx_newsletter_email_sender') === false) {
+            wp_schedule_event(
+                time(),
+                'cx_newsletter_send',
+                'cx_newsletter_email_sender'
+            );
+        }
+    }
+    
+    public function email_sender() {
+        $sender = new cron_emails_sender(
+            $this->database,
+            $this->tables,
+            $this->settings
+        );
+
+        $sender
+        ->prepare()
+        ->process();
+    }
+
+    public function admin() {
+        $this->update();
+        $this->connect_menu();
+
+        $config_panel = new config_panel_view(
+            $this->get_database(),
+            $this->get_settings(),
+            $this->get_templates(),
+            $this->get_tables()
+        );
+
+        $dashboard = new dashboard_view(
+            $this->get_database(),
+            $this->get_settings(),
+            $this->get_templates(),
+            $this->get_tables()
+        );
+
+        $manage_messages = new manage_messages_view(
+            $this->get_database(),
+            $this->get_settings(),
+            $this->get_templates(),
+            $this->get_tables()
+        );
+ 
+        $manage_customers = new manage_customers_view(
+            $this->get_database(),
+            $this->get_settings(),
+            $this->get_templates(),
+            $this->get_tables()
+        );       
+
+        $show_campaigns = new show_campaigns_view(
+            $this->get_database(),
+            $this->get_settings(),
+            $this->get_templates(),
+            $this->get_tables()
+        );       
+        
+        $manage_groups = new manage_groups_view(
+            $this->get_database(),
+            $this->get_settings(),
+            $this->get_templates(),
+            $this->get_tables()
+        );
+
+        $config_panel->connect();
+        $dashboard->connect();
+        $manage_messages->connect();
+        $manage_customers->connect();
+        $show_campaigns->connect();
+        $manage_groups->connect();
+    }
+
+    private function connect_menu() : void {
+        add_menu_page(
+            'cx-newsletter',
+            'cx-newsletter',
+            'customize',
+            'cx_newsletter',
+            '',
+            'dashicons-art'
+        );
+    }
+
+    public function update() {
+        $this
+        ->get_plugin_updater()
+        ->update();
+    }
+
+    private function get_plugin_updater() : plugin_updater {
+        return new plugin_updater(
+            $this->get_settings(),
+            $this->get_versions(),
+            $this->get_tables(),
+            $this->get_database()
+        );
+    }
+
+    public function install() {
+        $this
+        ->get_plugin_updater()
+        ->install();
+    }
+
+    public function remove() {
+        $this
+        ->get_plugin_updater()
+        ->remove();
+    }
+
+    protected function get_database() : wpdb {
+        return $this->database;
+    }
+
+    protected function get_templates() : \cx_appengine\templates {
+        return $this->templates;
+    }   
+
+    protected function get_settings() : settings { 
+        return $this->settings;
+    }
+
+    protected function get_tables() : table_names {
+        return $this->tables;
+    }
+
+    protected function get_versions() : database_versions_manager {
+        return $this->versions;
+    }
+    
+    private string $plugin;
+    private \cx_appengine\templates $templates;
+    private wpdb $database;
+    private table_names $tables;
+    private settings $settings;
+    private messages_get_endpoint $messages_get;
+    private messages_send_endpoint $messages_send;
+    private settings_check_endpoint $settings_check;
+    private database_versions_manager $versions;
+}

+ 32 - 0
components/10-rest_register.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace cx_newsletter;
+use \register_rest_route;
+
+class rest_register 
+implements rest_register_interface {
+    public function __construct(string $namespace) {
+        $this->namespace = $namespace;
+    }
+
+    public function register(rest_endpoint $endpoint) : self {
+        $parameters = [
+            'methods' => $endpoint->get_method(),
+            'callback' => [$endpoint, 'action']
+        ];
+
+        register_rest_route(
+            $this->get_namespace(),
+            $endpoint->get_route(),
+            $parameters
+        );
+
+        return $this;
+    }   
+
+    public function get_namespace() : string {
+        return $this->namespace;
+    }
+
+    private string $namespace;
+}

+ 20 - 0
components/11-group.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace cx_newsletter;
+
+class group
+extends database_item
+implements database_item_interface {
+    public ?string $name = null;
+
+    public function is_complete() : bool {
+        return $this->name !== null;
+    }
+
+    public function prepare_name(string $name) : void {
+        $name = str_replace(' ', '_', $name);
+        $name = strtolower($name);
+
+        $this->name = $name;
+    }
+}

+ 23 - 0
converters/1-campaign_type_converter.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace cx_newsletter;
+
+class campaign_type_converter
+extends enum_converter
+implements enum_converter_interface {
+    public function load_string(string $target) : self {
+        $this->target = match ($target) {
+            'SMS' => campaign_type::sms,
+            'EMAIL' => campaign_type::email
+        };
+
+        return $this;
+    }
+
+    public function get_string() : string {
+        return match ($this->target) {
+            campaign_type::sms => 'SMS',
+            campaign_type::email => 'EMAIL'
+        };
+    }
+}

+ 31 - 0
converters/2-messages_converter.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace cx_newsletter;
+
+class messages_converter
+extends database_converter
+implements database_converter_interface {
+    public function get_array() : array {
+        $this->check_target();
+
+        $result = [];
+        $result['title'] = $this->target->title;
+        $result['content'] = base64_encode($this->target->content);
+
+        if ($this->target->has_id()) {
+            $result['id'] = $this->target->get_id();
+        }
+
+        return $result;
+    }
+
+    public function load_array(array $target) : self {
+        $this->check_all_exists($target, [ 'title', 'content' ]);
+
+        $this->target = new message($this->id_or_null($target));
+        $this->target->title = $target['title'];
+        $this->target->content = base64_decode($target['content']);
+
+        return $this;
+    }
+}

+ 62 - 0
converters/3-customers-converter.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace cx_newsletter;
+
+class customers_converter
+extends database_converter
+implements database_converter_interface {
+    public function get_array() : array {
+        $this->check_target();
+
+        $result = [];
+        $result['company'] = $this->target->company;
+        $result['name'] = $this->target->name;
+        $result['surname'] = $this->target->surname;
+        $result['comments'] = $this->target->comments;
+        $result['email'] = $this->target->email;
+        $result['phone_number'] = $this->target->phone_number;
+        $result['tester'] = $this->target->tester;
+
+        if ($this->target->group !== null) {
+            $result['grouped'] = $this->target->group->get_id();
+        } else {
+            $result['grouped'] = null;
+        }
+    
+        if ($this->target->has_id()) {
+            $result['id'] = $this->target->get_id();
+        }   
+
+        return $result;
+    }
+
+    public function load_array(array $target) : self {
+        $this->check_all_exists($target, [
+            'company',
+            'name',
+            'surname',
+            'comments',
+            'email',
+            'phone_number',
+            'tester',
+            'grouped'
+        ]);
+
+        $this->target = new customer($this->id_or_null($target));
+        $this->target->company = $target['company'];
+        $this->target->name = $target['name'];
+        $this->target->surname = $target['surname'];
+        $this->target->comments = $target['comments'];
+        $this->target->email = $target['email'];
+        $this->target->phone_number = $target['phone_number'];
+        $this->target->tester = $target['tester'];
+
+        if ($target['grouped'] === null) {
+            $this->target->group = null;
+        } else {
+            $this->target->group = new group(intval($target['grouped']));
+        }
+
+        return $this;
+    }
+}

+ 64 - 0
converters/4-campaigns_converter.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace cx_newsletter;
+
+class campaigns_converter
+extends database_converter
+implements database_converter_interface {
+    public function load_array(array $target) : self {
+        $this->check_all_exists($target, ['message', 'type', 'finalized']);
+    
+        $type_converter = new campaign_type_converter();
+
+        $type = (
+            $type_converter
+            ->load_string($target['type'])
+            ->get_enum()
+        );
+
+        $id = intval($target['message']);
+        $message = new message($id);
+
+        $finalized = $this->date_or_null($target['finalized']);
+
+        $this->target = new campaign($this->id_or_null($target));
+
+        $this->target->type = $type;
+        $this->target->message = $message;
+        $this->target->finalized = $finalized;
+        $this->target->test = $target['test'];
+
+        if ($target['grouped'] !== null) {
+            $this->target->group = new group(intval($target['grouped']));
+        }
+
+        return $this;
+    }
+
+    public function get_array() : array {
+        $this->check_target();
+
+        $type_converter = new campaign_type_converter();
+        
+        $type = (
+            $type_converter
+            ->load_enum($this->target->type)
+            ->get_string()
+        );
+
+        $finalized = $this->string_from_date($this->target->finalized);
+
+        $target = [];
+        $target['message'] = $this->target->message->get_id();
+        $target['type'] = $type;
+        $target['finalized'] = $finalized;
+        $target['test'] = $this->target->test;
+        $target['grouped'] = null;
+
+        if ($this->target->group !== null and $this->target->group->has_id()) {
+            $target['grouped'] = $this->target->group->get_id();
+        }
+
+        return $target;
+    }
+}

+ 29 - 0
converters/5-group_converter.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace cx_newsletter;
+
+class group_converter
+extends database_converter 
+implements database_converter_interface {
+    public function get_array() : array {
+        $this->check_target();
+
+        $result = [];
+        $result['name'] = $this->target->name;
+
+        if ($this->target->has_id()) {
+            $result['id'] = $this->target->get_id();
+        }
+
+        return $result;
+    }
+
+    public function load_array(array $target) : self {
+        $this->check_all_exists($target, ['name']);
+
+        $this->target = new group($this->id_or_null($target));
+        $this->target->name = $target['name'];
+
+        return $this;
+    }
+}

+ 72 - 0
cx-newsletter.php

@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * Plugin Name: cx-newsletter
+ * Description: This is simple newsletter plugin, which could send content by
+ *              E-Mails or SMS-es.
+ * Text Domain: cx_newsletter
+ * Domain Path: /translates
+ * Authos: Cixo (Les Amis Reunis)
+ * License: MIT
+ * Version: 1.2.3
+ */
+
+namespace cx_newsletter;
+
+
+/** LIBS **/
+require_once(__DIR__.'/libs/CxAppengine/require.php');
+
+
+/** TEMPLATES TO RENDER **/
+const templates_dir = __DIR__.'/renders';
+
+
+/** REQUIRED COMPONENTS **/
+const import_from = [
+    'interfaces',
+    'traits',
+    'templates',
+    'validators',
+    'database',
+    'components',
+    'mappers',
+    'repositories',
+    'enums',
+    'converters',
+    'builders',
+    'activities',
+    'views',
+    'pages',
+    'endpoints'
+];
+
+
+/** LOAD **/
+foreach (import_from as $dir) {
+    foreach (scandir(__DIR__.'/'.$dir) as $file) {
+        if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
+            require_once(__DIR__.'/'.$dir.'/'.$file);
+        }
+    }
+}
+
+
+/** INIT **/
+$plugin_dir = dirname(\plugin_basename(__FILE__));
+$plugin = new plugin($plugin_dir);
+
+function activate() {
+    $plugin_dir = dirname(\plugin_basename(__FILE__));
+    $plugin = new plugin($plugin_dir);
+    $plugin->install();
+}
+
+function deactivate() {
+    $plugin_dir = dirname(\plugin_basename(__FILE__));
+    $plugin = new plugin($plugin_dir);
+    $plugin->remove();
+}
+
+\register_activation_hook(__FILE__, 'cx_newsletter\activate');
+\register_deactivation_hook(__FILE__, 'cx_newsletter\deactivate');

+ 26 - 0
database/01-table_names.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace cx_newsletter;
+
+class table_names 
+extends table_names_generator {
+    public function customers() : string {
+        return $this->add_prefixes('customers');
+    }
+
+    public function messages() : string {
+        return $this->add_prefixes('messages');
+    }
+
+    public function campaigns() : string {
+        return $this->add_prefixes('campaigns');
+    }
+
+    public function send_log() : string {
+        return $this->add_prefixes('send_log');
+    }
+    
+    public function groups() : string {
+        return $this->add_prefixes('groups');
+    }
+}

+ 46 - 0
database/02-database_versions_manager.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace cx_newsletter;
+use \get_option;
+use \update_option;
+use \delete_option;
+use \exception;
+
+class database_versions_manager {
+    public function get_installed() : ?int {
+        $version = get_option($this->get_database_version_name(), null);
+
+        if ($version === null) {
+            return null;
+        }
+
+        $version = intval($version);
+
+        if ($version === 0) {
+            return null;
+        }
+
+        return $version;
+    }
+
+    public function is_installed() : bool {
+        return $this->get_installed() !== null;
+    }   
+
+    public function set_installed(?int $version) : void {
+        if ($version === null) {
+            delete_option($this->get_database_version_name());
+            return;
+        }
+
+        if ($version <= 0) {
+            throw new exception('Version must be greaten than 0.');
+        }
+        
+        update_option($this->get_database_version_name(), strval($version));
+    }
+
+    private function get_database_version_name() : string {
+        return 'cx_newsletter_database_version';
+    }
+}

+ 132 - 0
database/03-database_installer.php

@@ -0,0 +1,132 @@
+<?php
+
+namespace cx_newsletter;
+use \exception;
+
+class database_installer
+extends database_builder
+implements database_builder_interface {
+    public function clean() : self {
+        $this->drop_table($this->get_tables()->send_log());
+        $this->drop_table($this->get_tables()->campaigns());
+        $this->drop_table($this->get_tables()->customers());
+        $this->drop_table($this->get_tables()->messages());
+        $this->drop_table($this->get_tables()->groups());
+
+        $this->get_versions()->set_installed(null);
+
+        return $this;
+    }
+
+    public function update() : self {
+        while (true) {
+            if ($this->increase_version() === false) {
+                return $this;
+            }
+        }
+    }
+
+    private function increase_version() : bool {
+        $version = $this->get_versions()->get_installed();
+
+        if ($version === null) {
+            $this->init();
+            return true;
+        }
+
+        if ($version === 1) {
+            $this->from_first_to_second();
+            return true;
+        }
+
+        if ($version === 2) {
+            return false;
+        }
+    }
+
+    private function from_first_to_second() : void {
+        $this->create_table($this->get_tables()->groups(), [
+            'id' => 'int auto_increment not null primary key',
+            'name' => 'char(128)'
+        ]);
+
+        $this->add_column(
+            $this->get_tables()->customers(),
+            'grouped', 
+            'int null default null' 
+        );
+
+        $this->add_foreign_key(
+            $this->get_tables()->customers(),
+            'grouped',
+            $this->get_tables()->groups(),
+            'id'
+        );
+
+        $this->add_column(
+            $this->get_tables()->campaigns(),
+            'grouped',
+            'int null default null'
+        );
+
+        $this->add_foreign_key(
+            $this->get_tables()->campaigns(),
+            'grouped',
+            $this->get_tables()->groups(),
+            'id'
+        );  
+
+        $this->get_versions()->set_installed(2);
+    }
+
+    public function init() : self {
+        if ($this->get_versions()->get_installed() !== null) {
+            $error = 'Can not initialize currently initialized database. ';
+            $error .= 'Clean it first.';
+
+            throw new exception($error);
+        }
+
+        $this->create_table($this->get_tables()->customers(), [
+            'id' => 'int auto_increment not null primary key',
+            'company' => 'char(128)',
+            'name' => 'char(64)',
+            'surname' => 'char(64)',
+            'comments' => 'char(255)',
+            'email' => 'char(64)',
+            'phone_number' => 'char(12)',
+            'tester' => 'bool',
+        ]);
+
+        $this->create_table($this->get_tables()->messages(), [
+            'id' => 'int auto_increment not null primary key',
+            'title' => 'char(128)',
+            'content' => 'text'
+        ]);
+
+        $this->create_table($this->get_tables()->campaigns(), [
+            'id' => 'int auto_increment not null primary key',
+            'message' => 'int not null',
+            'type' => 'enum("SMS", "EMAIL")',
+            'test' => 'bool',
+            'finalized' => 'datetime default null',
+            'foreign key (message)' =>   
+                'references '.$this->get_tables()->messages().'(id)'
+        ]);
+
+        $this->create_table($this->get_tables()->send_log(), [
+            'id' => 'int auto_increment not null primary key',
+            'campaign' => 'int not null',
+            'customer' => 'int not null',
+            'sended' => 'datetime not null',
+            'foreign key (campaign)' =>
+                'references '.$this->get_tables()->campaigns().'(id)',
+            'foreign key (customer)' =>
+                'references '.$this->get_tables()->customers().'(id)'
+        ]);
+
+        $this->get_versions()->set_installed(1);
+    
+        return $this;
+    }
+}

+ 105 - 0
endpoints/1-messages_get_endpoint.php

@@ -0,0 +1,105 @@
+<?php
+
+namespace cx_newsletter;
+use \wp_rest_request;
+
+class messages_get_endpoint 
+extends rest_endpoint
+implements rest_endpoint_interface {
+    use rest_endpoint_with_apikey;
+    
+    public function get_method() : string {
+        return 'GET';
+    }
+
+    public function get_route() : string {
+        return '/messages/';
+    }
+
+    public function action(wp_rest_request $request) : array {
+        if (!$this->check_request_apikey($request)) {
+            return $this->bad_apikey_response();
+        }
+
+        $response = new response_builder();
+        
+        return $response
+        ->set_code(response_code::SUCCESS)
+        ->set_status(response_status::SUCCESS)
+        ->set_message('List of messages to send.')
+        ->set_content($this->load_messages())
+        ->build();
+    }
+
+    private function load_messages() : array {
+        $this->prepare(); 
+
+        $next_campaign = $this->select_campaign();
+
+        if ($next_campaign === null) {
+            return [];
+        }
+        
+        $customers = $this->load_customers($next_campaign);
+        $list = [];
+
+        foreach ($customers as $customer) {
+            $count = [
+                'Message' => $next_campaign->message->content,
+                'PhoneNumber' => $customer->phone_number,
+                'Campaign' => $next_campaign->get_id()
+            ];
+
+            array_push($list, $count);
+
+            // FIX FOR CxSMS app
+            $this->send_log_repository->mark_send($next_campaign, $customer);
+        }
+
+        if (empty($list)) {
+            $next_campaign->finalize();
+            $this->campaigns_repository->save($next_campaign);
+        }
+
+        return $list;
+    }
+
+    private function select_campaign() : ?campaign {
+        return $this->campaigns_repository->load_next(campaign_type::sms);
+    }
+
+    private function load_customers(campaign $next) : array {
+        return $this->send_log_repository->load_all(
+            $next,
+            $this->get_settings()->get('sms_count'),
+            false
+        );
+    }
+
+    private function prepare() : void {
+        $this->campaigns_repository = new campaigns_repository(
+            $this->get_database(),
+            $this->get_tables()
+        );
+
+        $this->send_log_repository = new send_log_repository(
+            $this->get_database(),
+            $this->get_tables()
+        );
+        
+        $this->customers_mapper = new customers_mapper(
+            $this->get_database(),
+            $this->get_tables()
+        );
+
+        $this->messages_mapper = new messages_mapper(
+            $this->get_database(),
+            $this->get_tables()
+        );
+    }   
+
+    private campaigns_repository $campaigns_repository;
+    private send_log_repository $send_log_repository;
+    private customers_mapper $customers_mapper;
+    private messages_mapper $messages_mapper;
+}

+ 115 - 0
endpoints/2-messages_send_endpoint.php

@@ -0,0 +1,115 @@
+<?php
+
+namespace cx_newsletter;
+use \wp_rest_request;
+use \exception;
+
+class messages_send_endpoint
+extends rest_endpoint
+implements rest_endpoint_interface {
+    use rest_endpoint_with_apikey;
+
+    public function get_method() : string {
+        return 'POST';
+    }   
+
+    public function get_route() : string {
+        return '/message/send/';
+    }
+
+    public function action(wp_rest_request $request) : array {
+        if (!$this->check_request_apikey($request)) {
+            return $this->bad_apikey_response();
+        }
+
+        $response = new response_builder();
+        $params = $request->get_json_params();
+
+        if (!is_array($params)) {
+            return $response
+            ->set_code(response_code::ERROR)
+            ->set_status(response_status::INCOMPLETE)
+            ->set_message('Request must be JSON')
+            ->build();
+        }
+
+        if (!array_key_exists('PhoneNumber', $params)) {
+            return $response
+            ->set_code(response_code::NO_PHONE_NUMBER)
+            ->set_status(response_status::INCOMPLETE)
+            ->set_message('Phone number must be specified in the request.')
+            ->build();
+        }
+
+        if (!array_key_exists('Campaign', $params)) {
+            return $response
+            ->set_code(response_code::NO_PHONE_NUMBER)
+            ->set_status(response_status::INCOMPLETE)
+            ->set_message('Campaign must be specified in the request.')
+            ->build();
+        }
+
+        try {
+            $this->prepare();
+
+            $phone_number = $params['PhoneNumber'];
+            $campaign = intval($params['Campaign']);
+
+            $this->send($phone_number, $campaign);
+        } catch (exception $exception) {
+            return $response
+            ->set_code(response_code::ERROR)
+            ->set_status(response_status::ERROR)
+            ->set_message($exception->getMessage())
+            ->build();
+        }
+        
+        return $response
+        ->set_code(response_code::SUCCESS)
+        ->set_status(response_status::SUCCESS)
+        ->set_message('Successfull saved messages in send log.')
+        ->set_content('')
+        ->build();
+    }
+
+    private function send(string $phone_number, int $campaign_id) : void {
+        $filter = [ 'phone_number' => $phone_number ];
+        $customers = $this->customers_mapper->load_all($filter);
+
+        if (empty($customers)) {
+            throw new exception('Customer with phone number does not exists.');
+        }
+
+        $customer = $customers[0];
+        $campaign = new campaign($campaign_id);
+        $this->campaigns_repository->complete($campaign);
+        $this->send_log_repository->mark_send($campaign, $customer);
+    }
+
+    private function prepare() : void {
+        $this->campaigns_repository = new campaigns_repository(
+            $this->get_database(),
+            $this->get_tables()
+        );
+
+        $this->send_log_repository = new send_log_repository(
+            $this->get_database(),
+            $this->get_tables()
+        );
+        
+        $this->customers_mapper = new customers_mapper(
+            $this->get_database(),
+            $this->get_tables()
+        );
+
+        $this->messages_mapper = new messages_mapper(
+            $this->get_database(),
+            $this->get_tables()
+        );
+    }
+
+    private campaigns_repository $campaigns_repository;
+    private send_log_repository $send_log_repository;
+    private customers_mapper $customers_mapper;
+    private messages_mapper $messages_mapper;
+}

+ 26 - 0
endpoints/3-settings_check_endpoint.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace cx_newsletter;
+use \wp_rest_request;
+
+class settings_check_endpoint 
+extends rest_endpoint
+implements rest_endpoint_interface {
+    use rest_endpoint_with_apikey;
+
+    public function get_method() : string {
+        return 'GET';
+    }
+
+    public function get_route() : string {
+        return '/validate/';
+    }
+
+    public function action(wp_rest_request $request) : array {
+        if (!$this->check_request_apikey($request)) {
+            return [ 'Validation' => false ];
+        } 
+
+        return [ 'Validation' => true ];
+    }
+}

+ 8 - 0
enums/1-campaign_type.php

@@ -0,0 +1,8 @@
+<?php
+
+namespace cx_newsletter;
+
+enum campaign_type {
+    case sms;
+    case email;
+}

+ 9 - 0
enums/2-toast_type.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace cx_newsletter;
+
+enum toast_type:string {
+    case error = 'toast-error';
+    case warning = 'toast-warning';
+    case success = 'toast-success';
+}

+ 8 - 0
enums/3-filter_type.php

@@ -0,0 +1,8 @@
+<?php
+
+namespace cx_newsletter;
+
+enum filter_type:string {
+    case equal = '=';
+    case like = ' like ';
+}

+ 8 - 0
enums/4-filter_glue.php

@@ -0,0 +1,8 @@
+<?php
+
+namespace cx_newsletter;
+
+enum filter_glue:string {
+    case all = ' and ';
+    case single = ' or ';
+}

+ 11 - 0
enums/5-response_code.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace cx_newsletter;
+
+enum response_code: int {
+    case APIKEY_FAIL = 1;
+    case NO_PHONE_NUMBER = 2;
+    case NO_CAMPAIGN = 3;
+    case ERROR = 4;
+    case SUCCESS = 200;
+}

+ 9 - 0
enums/6-response_status.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace cx_newsletter;
+
+enum response_status: string {
+    case ERROR = 'Error';
+    case SUCCESS = 'Success';
+    case INCOMPLETE = 'Incomplete';
+}

+ 9 - 0
interfaces/01-database_builder_interface.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace cx_newsletter;
+
+interface database_builder_interface {
+    public function init() : self;
+    public function clean() : self;
+    public function update() : self;
+}

+ 10 - 0
interfaces/02-database_converter_interface.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace cx_newsletter;
+
+interface database_converter_interface {
+    public function load_object(object $target) : self;
+    public function load_array(array $target) : self;
+    public function get_array() : array;
+    public function get_object() : object;
+}

+ 9 - 0
interfaces/03-database_item_interface.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace cx_newsletter;
+
+interface database_item_interface {
+    public function has_id() : bool;
+    public function get_id() : int;
+    public function is_complete() : bool;
+}

+ 13 - 0
interfaces/04-mapper_interface.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace cx_newsletter;
+
+interface mapper_interface {
+    public function create(database_item $target) : database_item;
+    public function save(database_item $target) : database_item;
+    public function load_all(array $filters = [], ?int $limit = null) : array;
+    public function complete(database_item $target) : database_item;
+    public function remove(database_item $target) : void;
+    public function count(array $filters = []) : int;
+    public function find(string $phrase, ?int $limit = null) : array;
+}   

+ 10 - 0
interfaces/05-validator_interface.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace cx_newsletter;
+
+interface validator_interface {
+    public function load(mixed $target) : self;
+    public function check() : self;
+    public function is_valid() : bool;
+    public function error_on() : string;
+}

+ 10 - 0
interfaces/06-enum_converter_interface.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace cx_newsletter;
+
+interface enum_converter_interface {
+    public function load_enum(mixed $target) : self;
+    public function load_string(string $target) : self;
+    public function get_string() : string;
+    public function get_enum() : mixed;
+}

+ 9 - 0
interfaces/07-view_interface.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace cx_newsletter;
+
+interface view_interface {
+    public function connect() : void;
+    public function process() : void;
+    public function action() : void;
+}

+ 9 - 0
interfaces/08-settings_interface.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace cx_newsletter;
+
+interface settings_interface {
+    public function get(string $name) : string;
+    public function save(string $name, string $content) : self;
+    public function get_default(string $name) : string;
+}

+ 8 - 0
interfaces/09-builder_interface.php

@@ -0,0 +1,8 @@
+<?php
+
+namespace cx_newsletter;
+
+interface builder_interface {
+    public function is_complete() : bool;
+    public function get() : mixed;
+}

+ 8 - 0
interfaces/10-service_interface.php

@@ -0,0 +1,8 @@
+<?php
+
+namespace cx_newsletter;
+
+interface service_interface {
+    public function prepare() : self;
+    public function process() : self;
+}

+ 10 - 0
interfaces/11-rest_endpoint_interface.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace cx_newsletter;
+use \wp_rest_request;
+
+interface rest_endpoint_interface {
+    public function get_method() : string;
+    public function get_route() : string;
+    public function action(wp_rest_request $content) : mixed;
+}

+ 9 - 0
interfaces/12-rest_register_interface.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace cx_newsletter;
+
+interface rest_register_interface {
+    public function __construct(string $namespace);
+    public function get_namespace() : string;
+    public function register(rest_endpoint $endpoint) : self;
+}

+ 7 - 0
interfaces/13-plugin_worker_interface.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace cx_newsletter;
+use \wpdb;
+
+interface plugin_worker_interface {
+}

+ 1 - 0
libs/CxAppengine

@@ -0,0 +1 @@
+Subproject commit e43e1740e75fa86f4f9bd315f59079e18eace16e

+ 182 - 0
mappers/1-messages_mapper.php

@@ -0,0 +1,182 @@
+<?php
+
+namespace cx_newsletter;
+
+class messages_mapper
+extends mapper
+implements mapper_interface {
+    public function create(database_item $target) : database_item {
+        if ($target->has_id()) {
+            throw $this->get_has_id_exception();
+        }
+        
+        $converter = new messages_converter();
+        $converter->load_object($target);
+
+        $result = $this->get_database()->insert(
+            $this->get_tables()->messages(),
+            $converter->get_array()
+        );
+
+        if ($result === false) {
+            throw $this->get_exception('create');
+        }
+
+        return $this->complete(new message($this->get_database()->insert_id));
+    }
+
+    public function save(database_item $target) : database_item { 
+        if (!$target->has_id()) {
+            throw $this->get_blank_id_exception();
+        }
+
+        $converter = new messages_converter();
+        $converter->load_object($target);
+
+        $condition = [];
+        $condition['id'] = $target->get_id();
+
+        $result = $this->get_database()->update(
+            $this->get_tables()->messages(),
+            $converter->get_array(),
+            $condition
+        );
+
+        if ($result === false) {
+            throw $this->get_exception('save');
+        }
+
+        return $this->complete($target);
+    }
+
+    public function load_all(array $filters = [], ?int $limit = null) : array {
+        $query = new \cx_appengine\string_builder();
+        $query->push('select id, title, content ');
+        $query->push('from '.$this->get_tables()->messages().' ');
+        $query->push($this->parse_filters($filters)); 
+        $query->push($this->parse_limit($limit));
+
+        $result = $this->get_database()->get_results($query->get(), ARRAY_A);
+
+        if ($result === null) {
+            throw $this->get_exception('find');
+        }
+
+        $messages = [];
+        $converter = new messages_converter();
+
+        foreach ($result as $row) {
+            array_push($messages, $converter->load_array($row)->get_object());
+        }
+
+        return $messages;
+    }
+
+    public function complete(database_item $target) : database_item {
+        if (!$target->has_id()) {
+            throw $this->get_blank_id_exception();
+        }
+
+        $query = new \cx_appengine\string_builder();
+        $query->push('select id, title, content ');
+        $query->push('from '.$this->get_tables()->messages().' ');
+        $query->push('where id=%d');
+
+        $sql = $this->get_database()->prepare($query, $target->get_id());
+        $row = $this->get_database()->get_row($sql, ARRAY_A);
+
+        if ($row === null) {
+            throw $this->get_exception('complete');
+        }
+
+        $converter = new messages_converter();
+        $converter->load_array($row);
+
+        return $converter->get_object();
+    }
+
+    public function remove(database_item $target) : void {
+        if (!$target->has_id()) {
+            throw $this->get_blank_id_exception();
+        }
+
+        $repository = new campaigns_repository(
+            $this->get_database(),
+            $this->get_tables()
+        );
+
+        $campaigns = $repository->load_all(
+            [ 'message' => $target->get_id() ]
+        );
+
+        foreach ($campaigns as $campaign) {
+            $this->get_database()->delete(
+                $this->get_tables()->send_log(),
+                [ 'campaign' => $campaign->get_id() ]
+            );
+
+            $this->get_database()->delete(
+                $this->get_tables()->campaigns(),
+                [ 'id' => $campaign->get_id() ]
+            );  
+        }
+
+        $result = $this->get_database()->delete(
+            $this->get_tables()->messages(),
+            [ 'id' => $target->get_id() ]
+        );
+
+        if ($result === false) {
+            throw $this->get_exception('remove');
+        }
+    }
+
+    public function count(array $filters = []) : int {
+        $query = new \cx_appengine\string_builder();
+        $query->push('select count(id) ');
+        $query->push('from '.$this->get_tables()->messages().' ');
+        $query->push($this->parse_filters($filters));
+
+        $result = $this->get_database()->get_var($query->get());
+
+        if ($result === null) {
+            throw $this->get_exception('count');
+        }
+
+        return intval($result);
+    }
+
+    public function find(string $phrase, ?int $limit = null) : array {
+        $columns = [
+            'title' => '%'.$phrase.'%',
+            'content' => '%'.$phrase.'%'
+        ];
+
+        $filters = $this->parse_filters(
+            $columns, 
+            filter_type::like, 
+            filter_glue::single
+        );
+
+        $query = new \cx_appengine\string_builder();
+        $query->push('select id, title, content ');
+        $query->push('from '.$this->get_tables()->messages().' ');
+        $query->push($filters); 
+        $query->push($this->parse_limit($limit));
+
+        $result = $this->get_database()->get_results($query->get(), ARRAY_A);
+
+        if ($result === null) {
+            throw $this->get_exception('find');
+        }
+
+        $messages = [];
+        $converter = new messages_converter();
+
+        foreach ($result as $row) {
+            array_push($messages, $converter->load_array($row)->get_object());
+        }
+
+        return $messages;
+    }
+}

+ 167 - 0
mappers/2-customers_mapper.php

@@ -0,0 +1,167 @@
+<?php
+
+namespace cx_newsletter;
+
+class customers_mapper
+extends mapper
+implements mapper_interface {
+    public function create(database_item $target) : database_item {
+        if ($target->has_id()) {
+            throw $this->get_has_id_exception();
+        }
+
+        $converter = new customers_converter();
+        $converter->load_object($target);
+
+        $result = $this->get_database()->insert(
+            $this->get_tables()->customers(),
+            $converter->get_array()
+        );
+
+        if ($result === false) {
+            throw $this->get_exception('create');
+        }
+
+        return $this->complete(new customer($this->get_database()->insert_id));
+    }
+
+    public function save(database_item $target) : database_item {
+        if (!$target->has_id()) {
+            throw $this->get_blank_id_exception();
+        }
+
+        $converter = new customers_converter();
+        $converter->load_object($target);
+
+        $condition = [];
+        $condition['id'] = $target->get_id();
+
+        $result = $this->get_database()->update(
+            $this->get_tables()->customers(),
+            $converter->get_array(),
+            $condition
+        );
+
+        return $this->complete($target);
+    }
+
+    public function load_all(array $filters = [], ?int $limit = null) : array {
+        $query = new \cx_appengine\string_builder();
+        $query->push('select * ');
+        $query->push('from '.$this->get_tables()->customers().' ');
+        $query->push($this->parse_filters($filters));
+        $query->push($this->parse_limit($limit));
+
+        $result = $this->get_database()->get_results($query->get(), ARRAY_A);
+        
+        if ($result === null) {
+            throw $this->get_exception();
+        }
+
+        $customers = [];
+        $converter = new customers_converter();
+
+        foreach ($result as $row) {
+            array_push($customers, $converter->load_array($row)->get_object());
+        }
+
+        return $customers;
+    }
+
+    public function complete(database_item $target) : database_item {
+        if (!$target->has_id()) {
+            throw $this->get_blank_id_exception();
+        }
+
+        $query = new \cx_appengine\string_builder();
+        $query->push('select * ');
+        $query->push('from '.$this->get_tables()->customers().' ');
+        $query->push('where id=%d');
+
+        $sql = $this->get_database()->prepare($query, $target->get_id());
+        $row = $this->get_database()->get_row($sql, ARRAY_A);
+
+        if ($row === null) {
+            throw $this->get_exception('complete');
+        }
+
+        $converter = new customers_converter();
+        $converter->load_array($row);
+
+        return $converter->get_object();
+    }
+
+    public function remove(database_item $target) : void {
+        if (!$target->has_id()) {
+            throw $this->get_blank_id_exception();
+        }
+
+        $this->get_database()->delete(
+            $this->get_tables()->send_log(),
+            [ 'customer' => $target->get_id()]
+        );
+
+        $result = $this->get_database()->delete(
+            $this->get_tables()->customers(),
+            [ 'id' => $target->get_id() ]
+        );  
+
+        if ($result === false) {
+            throw $this->get_exception('remove');
+        }
+    }
+
+    public function count(array $filters = []) : int {
+        $query = new \cx_appengine\string_builder();
+        $query->push('select count(id) ');
+        $query->push('from '.$this->get_tables()->customers().' ');
+        $query->push($this->parse_filters($filters));
+
+        $result = $this->get_database()->get_var($query->get());
+
+        if ($result === null) {
+            throw $this->get_exception('count');
+        }
+
+        return intval($result);
+    }
+
+    public function find(string $phrase, ?int $limit = null) : array {
+        $columns = [
+            'name' => '%'.$phrase.'%',
+            'surname' => '%'.$phrase.'%',
+            'company' => '%'.$phrase.'%',
+            'comments' => '%'.$phrase.'%',
+            'email' => '%'.$phrase.'%',
+            'phone_number' => '%'.$phrase.'%',
+            'tester' => '%'.$phrase.'%',
+        ];
+
+        $filters = $this->parse_filters(
+            $columns,
+            filter_type::like,
+            filter_glue::single
+        );
+
+        $query = new \cx_appengine\string_builder();
+        $query->push('select * ');
+        $query->push('from '.$this->get_tables()->customers().' ');
+        $query->push($filters);
+        $query->push($this->parse_limit($limit));
+
+        $result = $this->get_database()->get_results($query->get(), ARRAY_A);
+        
+        if ($result === null) {
+            throw $this->get_exception();
+        }
+
+        $customers = [];
+        $converter = new customers_converter();
+
+        foreach ($result as $row) {
+            array_push($customers, $converter->load_array($row)->get_object());
+        }
+
+        return $customers;
+    }
+}

+ 192 - 0
mappers/3-groups_mapper.php

@@ -0,0 +1,192 @@
+<?php
+
+namespace cx_newsletter;
+use \cx_appengine\string_builder;
+use \exception;
+
+class groups_mapper
+extends mapper
+implements mapper_interface {
+    public function create(database_item $target) : database_item {
+        if ($target->has_id()) {
+            throw $this->get_has_id_exception();
+        }
+
+        if ($this->exists($target)) {
+            throw new exception('Group '.$target->name.' already exists.');
+        }
+
+        $converter = new group_converter();
+        $converter->load_object($target);
+
+        $result = $this->get_database()->insert(
+            $this->get_tables()->groups(),
+            $converter->get_array()
+        );
+
+        if ($result === false) {
+            throw $this->get_exception('create');
+        }
+
+        return $this->complete(new group($this->get_database()->insert_id));
+    }
+
+    private function exists(database_item $target) : bool {
+        $filter = [
+            'name' => $target->name
+        ];
+
+        $result = $this->load_all($filter);
+
+        if (empty($result)) {
+            return false;
+        }
+
+        if (!$target->has_id()) {
+            return true;
+        }
+
+        if ($result[0]->get_id() === $target->get_id()) {
+            return false;
+        }
+
+        return true;
+    }
+
+    public function save(database_item $target) : database_item { 
+        if (!$target->has_id()) {
+            throw $this->get_blank_id_exception();
+        }
+
+        if ($this->exists($target)) {
+            throw new exception('Group '.$target->name.' already exists.');
+        }
+
+        $converter = new group_converter();
+        $converter->load_object($target);
+
+        $condition = [];
+        $condition['id'] = $target->get_id();
+
+        $result = $this->get_database()->update(
+            $this->get_tables()->groups(),
+            $converter->get_array(),
+            $condition
+        );
+
+        if ($result === false) {
+            throw $this->get_exception('save');
+        }   
+        
+        return $this->complete($target);
+    }
+
+    public function load_all(array $filters = [], ?int $limit = null) : array {
+        $query = new string_builder();
+        $query->push('select id, name ');
+        $query->push('from '.$this->get_tables()->groups().' ');
+        $query->push($this->parse_filters($filters));
+        $query->push($this->parse_limit($limit));
+
+        $result = $this->get_database()->get_results($query->get(), ARRAY_A);
+
+        if ($result === null) {
+            throw $this->get_exception('find');
+        }
+
+        $groups = [];
+        $converter = new group_converter();
+
+        foreach ($result as $row) {
+            array_push($groups, $converter->load_array($row)->get_object());
+        }
+
+        return $groups;
+    }
+
+    public function complete(database_item $target) : database_item {
+        if (!$target->has_id()) {
+            throw $this->get_blank_id_exception();
+        }
+
+        $query = new string_builder();
+        $query->push('select id, name ');
+        $query->push('from '.$this->get_tables()->groups().' ');
+        $query->push('where id=%d');
+
+        $sql = $this->get_database()->prepare($query, $target->get_id());
+        $row = $this->get_database()->get_row($sql, ARRAY_A);
+
+        if ($row === null) {
+            throw $this->get_exception('complete');
+        }
+
+        $converter = new group_converter();
+        $converter->load_array($row);
+
+        return $converter->get_object();
+    }
+
+    public function remove(database_item $target) : void {
+        if (!$target->has_id()) {
+            throw $this->get_blank_id_exception();
+        }
+        
+        $result = $this->get_database()->delete(
+            $this->get_tables()->groups(),
+            [ 'id' => $target->get_id() ]
+        );
+
+        if ($result === false) {
+            throw $this->get_exception('remove');
+        }
+    }
+
+    public function count(array $filters = []) : int {
+        $query = new string_builder();
+        $query->push('select count(id) ');
+        $query->push('from '.$this->get_tables()->groups().' ');
+        $query->push($this->parse_filters($filters));
+
+        $result = $this->get_database()->get_var($query->get());
+
+        if ($result === null) {
+            throw $this->get_exception('count');
+        }
+
+        return intval($result);
+    }
+
+    public function find(string $phrase, ?int $limit = null) : array {
+        $columns = [
+            'name' => '%'.$phrase.'$'
+        ];
+
+        $filters = $this->parse_filters(
+            $columns,
+            filter_type::like,
+            filter_glue::single
+        );
+
+        $query = new string_builder();
+        $query->push('select id, name ');
+        $query->push('from '.$this->get_tables()->groups().' ');
+        $query->push($filters);
+        $query->push($this->parse_limit($limit));
+
+        $result = $this->get_database()->get_result($query->get(), ARRAY_A);
+
+        if ($result === null) {
+            throw $this->get_exception('find');
+        }
+
+        $groups = [];
+        $converter = new groups_converter();
+
+        foreach ($result as $row) {
+            array_push($groups, $converter->load_array($row)->get_object());
+        }
+
+        return $groups;
+    }
+}

+ 65 - 0
pages/1-customers_export_page.php

@@ -0,0 +1,65 @@
+<?php
+
+namespace cx_newsletter;
+
+class customers_export_page 
+extends service 
+implements service_interface {
+    public function process() : self {
+        return $this;
+    }
+
+    public function prepare() : self {
+        \add_rewrite_rule(
+            'cx-newsletter/customers-export$',
+            'index.php?'.$this->param.'=1',
+            'top'
+        );
+
+        global $wp_rewrite;
+        $wp_rewrite->flush_rules(true);
+
+        \add_filter('query_vars', [$this, 'add_query_var']);
+        \add_action('parse_request', [$this, 'parse_request']);
+
+        return $this;
+    }
+
+    public function add_query_var(array $vars) : array {
+        array_push($vars, $this->param);
+        return $vars;
+    }
+
+    public function parse_request($request) : void {
+        if (!array_key_exists($this->param, $request->query_vars)) {
+            return;
+        }
+
+        if (!\current_user_can('export')) {
+            echo('You must login as user that could export content.');
+            exit();
+        }   
+        
+        $mapper = new customers_mapper(
+            $this->get_database(),
+            $this->get_tables()
+        );
+
+        $converter = new customers_converter();
+
+        $all = [];
+
+        foreach ($mapper->load_all() as $customer) {
+            $customer = $converter->load_object($customer)->get_array();
+            unset($customer['id']);
+            array_push($all, $customer);
+        }
+
+        echo(json_encode($all));
+        header("Content-Type: application/json");
+        
+        exit();
+    }
+
+    private string $param = 'cx_newsletter_must_export_customers_now';
+}   

+ 11 - 0
renders/add_new_customer.html

@@ -0,0 +1,11 @@
+<form action="" method="post">
+    {{> customer_fields.html }}
+
+    <div class="group">
+        <input
+            type="submit"
+            name="add"
+            value="{{? Create }}"
+        >
+    </div>
+</form>

+ 85 - 0
renders/config_panel.html

@@ -0,0 +1,85 @@
+<style>
+    {{> styles/all.css }}
+</style>
+
+<div class="cx-newsletter-container">
+    {{$ toast_place }}
+    
+    <form action="" method="post">
+        <div class="section">
+            <div class="group center">
+                <span class="title">
+                    {{? cx-newsletter control panel }}
+                </span>
+            </div>
+            <div class="separator"></div>
+            <div class="group">
+                <label>
+                    {{? Setup E-Mail address from which mails would being send }}
+                </label>
+
+                <input
+                    type="email"
+                    name="source_address"
+                    placeholder="[email protected]"
+                    value="{{$ source_address }}"
+                >
+            </div>
+            <div class="group">
+                <label>
+                    {{? Setup E-Mail address where users would to reply }}
+                </label>
+
+                <input
+                    type="email"
+                    name="reply_to_address"
+                    placeholder="[email protected]"
+                    value="{{$ reply_to_address }}"
+                >
+            </div>
+            <div class="group">
+                <label>
+                    {{? How many E-Mails would be send in one transaction }}
+                </label>
+
+                <input
+                    type="number"
+                    name="email_count"
+                    placeholder="50"
+                    value="{{$ email_count }}"
+                >
+            </div>
+            <div class="group">
+                <label>
+                    {{? How many SMS-es would be send in one transaction }}
+                </label>
+
+                <input
+                    type="number"
+                    name="sms_count"
+                    placeholder="10"
+                    value="{{$ sms_count }}"
+                >
+            </div>
+            <div class="group">
+                <label>
+                    {{? Which Api Key should be used to access by the SMS gate }}
+                </label>
+
+                <input 
+                    type="text"
+                    name="apikey"
+                    placeholder="TEST_APIKEY_TEST_APIKEY"
+                    value="{{$ apikey }}"
+                >
+            </div>
+            <div class="group center">
+                <input 
+                    type="submit"
+                    name="save"
+                    value="{{? Save }}"
+                >
+            </div>
+        </div>
+    </form>
+</div>

+ 101 - 0
renders/customer_fields.html

@@ -0,0 +1,101 @@
+<div class="group">
+    <label>
+        {{? Name }}
+    </label>
+
+    <input 
+        type="text"
+        name="name"
+        placeholder="{{? John }}"
+        value="{{$ name }}"
+    >
+</div>
+
+<div class="group">
+    <label>
+        {{? Surname }}
+    </label>
+
+    <input
+        type="text"
+        name="surname"
+        placeholder="{{? Snow }}"
+        value="{{$ surname }}"
+    >
+</div>
+
+<div class="group"> 
+    <label>
+        {{? Company }}
+    </label>
+
+    <input 
+        type="text"
+        name="company"
+        placeholder="{{? Cixo Electronic }}"
+        value="{{$ company }}"
+    >
+</div>
+
+<div class="group">
+    <label>
+        {{? Comments }}
+    </label>
+
+    <input
+        type="text"
+        name="comments"
+        placeholder="{{? Nice customer }}"
+        value="{{$ comments }}"
+    >
+</div>
+
+<div class="group">
+    <label>
+        {{? E-Mail }}
+    </label>
+
+    <input 
+        type="email"
+        name="email"
+        value="{{$ email }}"
+        placeholder="{{? [email protected] }}"
+        
+    >
+</div>
+
+<div class="group">
+    <label>
+        {{? Phone number }}
+    </label>
+
+    <input  
+        type="number"
+        name="phone_number"
+        value="{{$ phone_number }}"
+        placeholder="{{? 123456789 }}"
+    >
+</div>
+
+<div class="group">
+    <label>
+        {{? Only tester }}
+    </label>
+
+    <input
+        type="checkbox"
+        name="tester"
+        value="true"
+        {{$ tester }}
+    >
+</div>
+
+<div class="group">
+    <label for="group">
+        {{? User group }}
+    </label>
+    
+    <select name="group" id="group">
+        {{$ groups }}
+    </select>
+</div>

+ 32 - 0
renders/dashboard.html

@@ -0,0 +1,32 @@
+<style>
+    {{> styles/all.css }}
+</style>    
+
+<div class="cx-newsletter-container">
+    <div class="section">
+        <span class="title">
+            {{? Dashboard }}
+        </span>
+
+        <span class="separator"></span>
+
+        <div class="column">
+            <p>{{? Total messages }}: <b>{{$ messages_count }}</b></p>
+            <p>{{? Total customers }}: <b>{{$ customers_count }}</b></p>
+        </div>
+    </div>
+
+    <div class="section">
+        <span class="title">
+            {{? Useable actions }}
+        </span>
+
+        <span class="separator"></span>
+
+        <div class="group">
+            <a href="{{$ manage_messages_link }}">{{? Manage messages }}</a>
+            <a href="{{$ manage_customers_link }}">{{? Manage customers }}</a>
+            <a href="{{$ show_campaigns_link }}">{{? Show campaigns }}</a>
+        </div>
+    </div>
+</div>

+ 78 - 0
renders/edit_message.html

@@ -0,0 +1,78 @@
+<style>
+    {{> styles/all.css }}
+</style>
+
+<script type="text/javascript">
+    {{> scripts/preview.js }}
+</script>
+
+<div class="cx-newsletter-container" style="flex-direction: row; flex-wrap: wrap;">
+    {{$ toast_place }}
+    
+    <form action="" method="post">
+        <div class="section">
+            <span class="title">
+                {{? Message editor }}
+            </span> 
+
+            <span class="separator"></span>
+
+            <input
+                type="text"
+                name="id"
+                value="{{$ id }}"
+                style="display: none;"
+            >
+
+            <div class="group">
+                <label>
+                    {{? Title }}
+                </label>
+
+                <input 
+                    type="text"
+                    name="title"
+                    placeholder="{{? Title... }}"
+                    value="{{$ title }}"
+                >
+            </div>
+
+            <div class="column">
+                <div class="editor">
+                    {{$ editor }}
+                </div>
+            </div>
+
+            <div class="group">
+                <input
+                    type="submit"
+                    name="save"
+                    value="{{? Save }}"
+                >
+                
+                <input
+                    type="submit"
+                    name="return"
+                    value="{{? Return }}"
+                >
+
+                <input 
+                    type="button"
+                    name="reload"
+                    value="{{? Reload preview }}"
+                    class="reload"
+                >
+            </div>
+        </div>
+    </form>
+
+    <div class="section">
+        <span class="title">
+            {{? Preview }}
+        </span>
+
+        <span class="separator"></span>
+
+        <iframe class="preview"></iframe>
+    </div>
+</div>

+ 72 - 0
renders/edit_message.html.old

@@ -0,0 +1,72 @@
+<style>
+    {{> styles/all.css }}
+</style>
+
+<script type="text/javascript">
+    {{> scripts/preview.js }}
+</script>
+
+<div class="cx-newsletter-container">
+    {{$ toast_place }}
+    
+    <form action="" method="post">
+        <div class="section">
+            <span class="title">
+                {{? Message editor }}
+            </span> 
+
+            <span class="separator"></span>
+
+            <input
+                type="text"
+                name="id"
+                value="{{$ id }}"
+                style="display: none;"
+            >
+
+            <div class="group">
+                <label>
+                    {{? Title }}
+                </label>
+
+                <input 
+                    type="text"
+                    name="title"
+                    placeholder="{{? Title... }}"
+                    value="{{$ title }}"
+                >
+            </div>
+
+            <div class="column">
+                <label>
+                    {{? Content }}
+                </label>
+
+                <textarea
+                    name="content"
+                    placeholder="{{? Content }}"
+                    class="preview-source"
+                    id="cx_newsletter_editor"
+                >{{$ content }}</textarea>
+            </div>
+
+            <div class="group">
+                <input
+                    type="submit"
+                    name="save"
+                    value="{{? Save }}"
+                >
+                
+                <input
+                    type="submit"
+                    name="return"
+                    value="{{? Return }}"
+                >
+            </div>
+        </div>
+    </form>
+
+    <div class="section">
+        <div class="preview"></div>
+    </div>
+</div>

+ 19 - 0
renders/import_customers.html

@@ -0,0 +1,19 @@
+<style>
+    {{> styles/all.css }}
+</style>
+
+<div class="cx-newsletter-container">
+    {{$ toast_place }}
+
+    <div class="section">
+        <div class="group">
+            <form action="" method="post">
+                <input 
+                    type="submit" 
+                    name="" 
+                    value="{{? Return }}"
+                >
+            </form>
+        </div>
+    </div>
+</div>

+ 112 - 0
renders/manage_customers.html

@@ -0,0 +1,112 @@
+<style>
+    {{> styles/all.css }}
+</style>
+
+<div class="cx-newsletter-container">
+    {{$ toast_place }}
+   
+    <div class="section">
+        <span class="title">
+            {{? Global actions }}
+        </span>
+
+        <span class="separator"></span>
+
+        <form action="" method="post" enctype="multipart/form-data">
+            <div class="group">
+                <label>
+                    {{? Import users from list to group }}
+                </label>
+
+                <select name="group" id="group">
+                    {{$ groups }}
+                </select>
+            </div>
+
+            <div class="group">
+                <label>
+                    {{? File with customers }}
+                </label>
+
+                <input
+                    type="file"
+                    name="upload"
+                >  
+            </div>
+            
+            <div class="group">
+                <input 
+                    type="submit"
+                    name="import"
+                    value="{{? Import }}"
+                >
+    
+                <input 
+                    type="submit"
+                    name="clean"
+                    value="{{? Clean }}"
+                >
+                
+                <a href="{{$ export }}" download="custoomers-export.json">
+                    {{? Export }}
+                </a>
+            </div>
+        </form>
+    </div>
+
+    <div class="section">
+        <span class="title">
+            {{? Add new customer }}
+        </span>
+
+        <span class="separator"></span>
+        
+        {{> add_new_customer.html }}
+    </div>
+
+    <div class="section">
+        <span class="title">
+            {{? Customers filter }}
+        </span> 
+
+        <span class="separator"></span>
+
+        <form action="" method="post">
+            <div class="group">
+                <label>
+                    {{? Show customers like }}
+                </label>
+
+                <input 
+                    type="text"
+                    name="filter"
+                    placeholder="{{? Like... }}"
+                    value="{{$ filter }}"
+                >
+            </div>
+
+            <div class="group">
+                <label>
+                    {{? Maximum number of customers }}
+                </label>
+
+                <input
+                    type="number"
+                    name="limit"
+                    value="{{$ limit }}"
+                    placeholder="50"
+                >
+            </div>
+
+            <div class="group">
+                <input
+                    type="submit"
+                    name="search"
+                    value="{{? Search }}"
+                >
+            </div>
+        </form>
+    </div>
+
+    {{$ all_customers }}
+</div>

+ 42 - 0
renders/manage_groups.html

@@ -0,0 +1,42 @@
+<style>
+    {{> styles/all.css }}
+</style>
+
+<div class="cx-newsletter-container">
+    {{$ toast_place }}
+
+    <div class="section">
+        <span class="title">
+            {{? Add group }}
+        </span>
+
+        <span class="separator"></span>
+
+        <div class="column">
+            <form method="post">    
+                <div class="group">
+                    <label for="name">{{? New group }}</label>
+                    <input
+                        type="text"
+                        name="name"
+                        id="name"
+                        placeholder="{{? best_group }}">
+                </div>
+                <input
+                    type="submit"
+                    name="add"
+                    value="{{? Add }}">
+            </form>
+        </div>
+    </div>
+
+    <div class="section">
+        <span class="title">
+            {{? Current groups }}
+        </span>
+
+        <span class="separator"></span>
+
+        {{$ groups }}
+    </div>
+</div>

+ 27 - 0
renders/manage_messages.html

@@ -0,0 +1,27 @@
+<style>
+    {{> styles/all.css }}
+</style>
+
+<div class="cx-newsletter-container">
+    {{$ toast_place }}
+    
+    <div class="section">
+        <span class="title">
+            {{? Manage messages }}
+        </span>
+
+        <span class="separator"></span>
+   
+        <form action="" method="post">
+            <div class="group">
+                <input 
+                    type="submit"
+                    name="create"
+                    value="{{? Create message }}"
+                >
+            </div>
+        </form>
+    </div>
+    
+    {{$ messages_place }}
+</div>

+ 71 - 0
renders/manage_messages_single_message.html

@@ -0,0 +1,71 @@
+<div class="section">
+    <div class="group">
+        <p>{{? Message }} #{{$ id }}</p>
+        <p><b>{{$ title }}</b></p>
+    </div>
+
+    <span class="separator"></span>
+
+    <form action="" method="post">
+       <input  
+            type="text"
+            value="{{$ id }}"
+            name="id"
+            style="display: none;"
+        >
+
+        <div class="group">
+            <label for="group">
+                {{? Send to group }}
+            </label>
+
+            <select name="group" id="group">
+                {{$ groups }}
+            </select>
+        </div>
+
+        <div class="group">
+            <div class="column">
+                <input
+                    type="submit"
+                    name="edit"
+                    value="{{? Edit }}"
+                >
+
+                <input 
+                    type="submit"
+                    name="remove"
+                    value="{{? Remove }}"
+                >
+            </div>
+
+            <div class="column">
+                <input
+                    type="submit"
+                    name="test_via_email"
+                    value="{{? Test via E-Mail }}"
+                >
+
+                <input 
+                    type="submit"
+                    name="test_via_sms"
+                    value="{{? Test via SMS }}"
+                >
+            </div>
+        
+            <div class="column">
+                <input
+                    type="submit"
+                    name="send_via_email"
+                    value="{{? Send via E-Mail }}"
+                >
+
+                <input 
+                    type="submit"
+                    name="send_via_sms"
+                    value="{{? Send via SMS }}"
+                >
+            </div>
+        </div>
+    </form>
+</div>

+ 7 - 0
renders/only_toast_site.html

@@ -0,0 +1,7 @@
+<style>
+    {{> styles/all.css }}
+</style>
+
+<div class="cx-newsletter-container">
+    {{> toast.html }}
+</div>

+ 15 - 0
renders/scripts/preview.js

@@ -0,0 +1,15 @@
+document.addEventListener("DOMContentLoaded", () => {
+    const container = document.querySelector(".cx-newsletter-container");
+    const preview = container.querySelector(".preview");
+    const reload = container.querySelector(".reload");
+
+    reload.addEventListener("click", () => { reload_it(); });
+
+    setTimeout(() => { reload_it(); }, 500);
+
+    const reload_it = () => {
+        if (tinymce.activeEditor) {
+            preview.srcdoc = tinymce.activeEditor.getContent();
+        }
+    };
+});

+ 19 - 0
renders/send_message.html

@@ -0,0 +1,19 @@
+<style>
+    {{> styles/all.css }}
+</style>
+
+<div class="cx-newsletter-container">
+    {{$ toast_place }}
+
+    <div class="section">
+        <div class="group">
+            <form action="" method="post">
+                <input 
+                    type="submit" 
+                    name="" 
+                    value="{{? Return }}"
+                >
+            </form>
+        </div>
+    </div>
+</div>

+ 7 - 0
renders/show_campaigns.html

@@ -0,0 +1,7 @@
+<style>
+    {{> styles/all.css }}
+</style>
+
+<div class="cx-newsletter-container">
+    {{$ campaigns }}
+</div>

+ 14 - 0
renders/single_campaign.html

@@ -0,0 +1,14 @@
+<div class="section">
+    <div class="column">
+        <span class="title">
+            {{? Campaign }} #{{$ id }}
+        </span>
+
+        <p>{{? Only test }}: <b>{{$ test }}</b></p>
+        <p>{{? Message title }}: <b>{{$ title }}</b></p>
+        <p>{{? Finalized }}: <b>{{$ finalized }}</b></p>
+        <p>{{? Type }}: <b>{{$ type }}</b></p>
+        <p>{{? Sended }}: <b>{{$ sended }}</b></p>
+        <p>{{? To send }}: <b>{{$ to_send }}</b></p>
+    </div>
+</div>

+ 32 - 0
renders/single_customer.html

@@ -0,0 +1,32 @@
+<div class="section">
+    <span class="title">
+        {{? Customer }}: #{{$ id }}
+    </span>
+
+    <span class="separator"></span>
+
+    <form action="" method="post">
+        <input 
+            type="text"
+            name="id"
+            value="{{$ id }}"
+            style="display: none;"
+        >
+
+        {{> customer_fields.html }}
+
+        <div class="group">
+            <input
+                type="submit"
+                name="save"
+                value="{{? Save }}"
+            >
+
+            <input
+                type="submit"
+                name="remove"
+                value="{{? Remove }}"
+            >
+        </div>
+    </form>
+</div>

+ 39 - 0
renders/single_group.html

@@ -0,0 +1,39 @@
+<div class="section">
+    <form action="" method="post">
+        <div class="column">
+            <span class="title">
+                {{? Group }} #{{$ id }}
+            </span>
+
+            <input 
+                type="number"
+                style="display: none;"
+                value="{{$ id }}"
+                name="id"
+            />
+            
+            <div class="group">
+                <label>{{? Group name }}</label>
+                
+                <input 
+                    type="text"
+                    placeholder="best_group"
+                    name="name"
+                    value="{{$ name }}"
+                />
+
+                <input
+                    type="submit"
+                    name="delete"
+                    value="{{? Delete }}"
+                />
+
+                <input
+                    type="submit"
+                    name="save"
+                    value="{{? Save }}"
+                />
+            </div>
+        </div>
+    </form>
+</div>

+ 1 - 0
renders/single_group_option.html

@@ -0,0 +1 @@
+<option value="{{$ group_id }}" {{$ selected }}>{{$ group_name }}</option>

+ 10 - 0
renders/styles/all.css

@@ -0,0 +1,10 @@
+{{> colors.css }}
+{{> container.css }}
+{{> section.css }}
+{{> group.css }}
+{{> inputs.css }}
+{{> font.css }}
+{{> separator.css }}
+{{> toast.css }}
+{{> column.css }}
+{{> preview.css }}

+ 6 - 0
renders/styles/colors.css

@@ -0,0 +1,6 @@
+.cx-newsletter-container * {
+    --primary-color: #07393C;
+    --secondary-color: #2C666E;
+    --background-color: #F0EDEE;
+    --font-color: #0A090C;
+}

+ 8 - 0
renders/styles/column.css

@@ -0,0 +1,8 @@
+.cx-newsletter-container .column {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    gap: 0.5em;
+    width: fit-content;
+}

+ 13 - 0
renders/styles/container.css

@@ -0,0 +1,13 @@
+.cx-newsletter-container, .cx-newsletter-container form {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-around;
+    align-items: center;
+    gap: 2rem;
+    margin-top: 2rem;
+    width: 100%;
+}
+
+.cx-newsletter-container form {
+    margin-top: 0px;
+}

+ 9 - 0
renders/styles/font.css

@@ -0,0 +1,9 @@
+.cx-newsletter-container .title {
+    color: var(--primary-color);
+    font-weight: bold;
+    font-size: 1.2em;
+}
+
+.cx-newsletter-container p, .cx-newsletter-container label {
+    color: var(--font-color);
+}

+ 17 - 0
renders/styles/group.css

@@ -0,0 +1,17 @@
+.cx-newsletter-container .group {
+    width: 100%;
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    align-items: center;
+    justify-content: center;
+    gap: 2rem;
+}
+
+.cx-newsletter-container .group label {
+    max-width: 300px;
+}
+
+.cx-newsletter-container .group.center {
+    justify-content: center;
+}

+ 70 - 0
renders/styles/inputs.css

@@ -0,0 +1,70 @@
+.cx-newsletter-container input {
+    outline: none !important;
+    border: none;
+    box-shadow: none !important;
+    font-size: 1em;
+    text-align: center;
+}
+
+.cx-newsletter-container input[type="text"],
+.cx-newsletter-container input[type="number"],
+.cx-newsletter-container input[type="email"],
+.cx-newsletter-container input[type="password"] {
+    border: none;
+    border-bottom: 2px solid  var(--primary-color);
+    border-radius: 0px;
+    background-color: var(--background-color);
+    color: var(--secondary-color);
+    transition:
+        border-color 0.5s,
+        transform 0.5s;
+}
+
+.cx-newsletter-container input[type="text"]:focus,
+.cx-newsletter-container input[type="number"]:focus,
+.cx-newsletter-container input[type="email"]:focus,
+.cx-newsletter-container input[type="password"]:focus {
+    border-color: var(--secondary-color);
+    transform: scaleX(1.1);
+}
+
+.cx-newsletter-container input[type="submit"],
+.cx-newsletter-container input[type="button"],
+.cx-newsletter-container a {
+    text-decoration: none;
+    display: block;
+    background-color: var(--primary-color);
+    color: var(--background-color);
+    padding: 1em 2em;
+    border-radius: 1em;
+    border: none;
+    transition:
+        background-color 0.5s;
+}
+
+.cx-newsletter-container input[type="submit"]:hover,
+.cx-newsletter-container input[type="button"]:hover,
+.cx-newsletter-container a:hover {
+    background-color: var(--secondary-color);
+}
+
+.cx-newsletter-container textarea {
+    width: 90%;
+    box-sizing: border-box;
+    min-height: 500px;
+    padding: 1rem;
+    border: 3px solid var(--secondary-color);
+    border-radius: 1rem;
+    color: var(--font-color);
+    background-color: inherit;
+    transition:
+        transform 0.5s;
+}
+
+.cx-newsletter-container textarea:focus {
+    transform: scaleX(1.05);
+}
+
+.cx-newsletter-container input[type="checkbox"] {
+    border: 2px solid var(--primary-color);
+}

+ 8 - 0
renders/styles/preview.css

@@ -0,0 +1,8 @@
+.preview {
+    width: 100%;
+    height: 600px;
+    background-color: white;
+    color: black;
+    border: 3px solid black;
+}
+

+ 13 - 0
renders/styles/section.css

@@ -0,0 +1,13 @@
+.cx-newsletter-container .section {
+    width: 90%;
+    max-width: 800px;
+    box-sizing: border-box;
+    padding: 2rem;
+    border-radius: 2rem;
+    border: 3px solid var(--primary-color);
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    gap: 2rem;
+}

+ 6 - 0
renders/styles/separator.css

@@ -0,0 +1,6 @@
+.cx-newsletter-container .separator {
+    width: 100%;
+    height: 3px;
+    display: block;
+    background-color: var(--primary-color);
+}

+ 30 - 0
renders/styles/toast.css

@@ -0,0 +1,30 @@
+.cx-newsletter-container .toast {
+    width: fit-content;
+    max-width: 500px;
+    min-width: 300px;
+    padding: 2rem;
+    border-radius: 2rem;
+    border: 3px solid var(--secondary-color);
+    color: var(--secondary-color);
+}
+
+.cx-newsletter-container .toast p {
+    font-weight: bold;
+    text-align: center;
+    color: inherit;
+}
+
+.cx-newsletter-container .toast.toast-error {
+    color: red;
+    border-color: red;
+}
+
+.cx-newsletter-container .toast.toast-warning {
+    color: orange;
+    border-color: orange;
+}
+
+.cx-newsletter-container .toast.toast-success {
+    color: green;
+    border-color: green;
+}

+ 3 - 0
renders/toast.html

@@ -0,0 +1,3 @@
+<div class="toast {{$ type }}">
+    <p>{{$ content }}</p>
+</div>

+ 132 - 0
repositories/1-campaigns_repository.php

@@ -0,0 +1,132 @@
+<?php
+
+namespace cx_newsletter;
+
+class campaigns_repository
+extends repository {
+    public function create(campaign $target) : campaign {
+        if ($target->has_id()) {
+            throw $this->get_has_id_exception();
+        }
+
+        $converter = new campaigns_converter();
+        $converter->load_object($target);
+
+        $result = $this->get_database()->insert(
+            $this->get_tables()->campaigns(),
+            $converter->get_array()
+        );
+
+        if ($result === false) {
+            throw $this->get_exception('create');
+        }
+
+        return $this->complete(
+            new campaign($this->get_database()->insert_id)
+        );
+    }
+
+    public function complete(campaign $target) : campaign { 
+        if (!$target->has_id()) {
+            throw $this->get_blank_id_exception();
+        }
+
+        $query = new \cx_appengine\string_builder();
+        $query->push('select * ');
+        $query->push('from '.$this->get_tables()->campaigns().' ');
+        $query->push('where id=%d');
+
+        $sql = $this->get_database()->prepare($query, $target->get_id());
+        $row = $this->get_database()->get_row($sql, ARRAY_A);
+
+        if ($row === null ) {
+            throw $this->get_exception('complete');
+        }
+
+        $converter = new campaigns_converter();
+        $converter->load_array($row);
+
+        $target = $converter->get_object();
+        
+        $mapper = new messages_mapper(
+            $this->get_database(),
+            $this->get_tables()
+        );
+
+        $target->message = $mapper->complete($target->message);
+
+        return $target;
+    }
+
+    public function load_all(array $filters = [], ?int $limit = null) : array {
+        $query = new \cx_appengine\string_builder();
+        $query->push('select * ');
+        $query->push('from '.$this->get_tables()->campaigns().' ');
+        $query->push($this->parse_filters($filters));
+        $query->push($this->parse_limit($limit));
+        $query->push('order by id desc');
+
+        $result = $this->get_database()->get_results($query->get(), ARRAY_A);
+
+        if ($result === null) {
+            throw $this->get_exception('load_all');
+        }
+
+        $campaigns = [];
+        $converter = new campaigns_converter();
+
+        $mapper = new messages_mapper(
+            $this->get_database(),
+            $this->get_tables()
+        );
+
+        foreach ($result as $row) {
+            $target = $converter->load_array($row)->get_object();
+            $target->message = $mapper->complete($target->message);
+
+            array_push($campaigns, $target);
+        }
+
+        return $campaigns;
+    }
+
+    public function load_next(campaign_type $type) : ?campaign {
+        $converter = new campaign_type_converter();
+
+        $filters = [];
+        $filters['finalized'] = null;
+        $filters['type'] = $converter->load_enum($type)->get_string();
+
+        $nexts = $this->load_all($filters);
+        
+        if (empty($nexts)) {
+            return null;
+        }
+        
+        return $nexts[0];
+    }
+
+    public function save(campaign $target) : ?campaign {
+        if (!$target->has_id()) {
+            throw $this->get_blank_id_exception();
+        }   
+
+        $converter = new campaigns_converter();
+        $converter->load_object($target);
+
+        $condition = [];
+        $condition['id'] = $target->get_id();
+
+        $result = $this->get_database()->update(
+            $this->get_tables()->campaigns(),
+            $converter->get_array(),
+            $condition
+        );
+
+        if ($result === false) {
+            throw $this->get_exception('save');
+        }
+
+        return $this->complete($target);
+    }
+}

+ 130 - 0
repositories/2-send_log_repository.php

@@ -0,0 +1,130 @@
+<?php
+
+namespace cx_newsletter;
+
+class send_log_repository 
+extends repository {
+    public function load_all(
+        campaign $target, 
+        ?int $limit = null, 
+        ?bool $sended = null
+    ) : array {
+        if (!$target->has_id()) {
+            throw $this->get_blank_id_exception();
+        }
+
+        $campaigns = $this->get_tables()->campaigns();
+        $customers = $this->get_tables()->customers();
+        $send_log = $this->get_tables()->send_log();
+
+        $query = new \cx_appengine\string_builder();
+        $query->push('select '.$customers.'.id as customer ');
+        $query->push('from '.$campaigns.' ');
+        $query->push('inner join '.$customers.' ');
+        $query->push('left outer join '.$send_log.' ');
+        $query->push('on '.$customers.'.id='.$send_log.'.customer ');
+        $query->push('and '.$campaigns.'.id='.$send_log.'.campaign ');
+        $query->push('where '.$campaigns.'.id='.$target->get_id().' ');
+        $query->push('and '.$campaigns.'.test='.$customers.'.tester ');
+
+        if ($target->group !== null and $target->group->has_id()) {
+            $query->push('and '.$customers.'.grouped=');
+            $query->push(strval($target->group->get_id().' '));
+        }
+
+        if ($target->type === campaign_type::email) {
+            $query->push('and '.$customers.'.email != "" ');
+        } elseif ($target->type === campaign_type::sms) {
+            $query->push('and '.$customers.'.phone_number != "" ');
+        }
+        
+        if ($sended === false) {
+            $query->push('and '.$send_log.'.id is NULL ');
+        } elseif ($sended === true) {
+            $query->push('and '.$send_log.'.id is not NULL ');
+        }
+
+        $query->push($this->parse_limit($limit));
+
+        $result = $this->get_database()->get_results($query->get(), ARRAY_A);
+
+        if ($result === null) {
+            throw $this->get_exception('load_all');
+        }
+
+        $next = [];
+        $mapper = new customers_mapper(
+            $this->get_database(),
+            $this->get_tables()
+        );
+
+        foreach ($result as $row) {
+            $customer = new customer($row['customer']);
+            $customer = $mapper->complete($customer);
+
+            array_push($next, $customer);
+        }
+
+        return $next;
+    }
+
+    public function mark_send(campaign $from, customer $who) : self {
+        if (!$from->has_id() or !$who->has_id()) {
+            throw $this->get_blank_id_exception();
+        }
+
+        $log = [];
+        $log['campaign'] = $from->get_id();
+        $log['customer'] = $who->get_id();
+        $log['sended'] = \current_time('mysql', 1);
+
+        $this->get_database()->insert($this->get_tables()->send_log(), $log);
+    
+        return $this;
+    }
+
+    public function count(campaign $target, ?bool $sended = null) : int {
+        if (!$target->has_id()) {
+            throw $this->get_blank_id_exception();
+        }
+
+        $campaigns = $this->get_tables()->campaigns();
+        $customers = $this->get_tables()->customers();
+        $send_log = $this->get_tables()->send_log();
+
+        $query = new \cx_appengine\string_builder();
+        $query->push('select count('.$customers.'.id) ');
+        $query->push('from '.$campaigns.' ');
+        $query->push('inner join '.$customers.' ');
+        $query->push('left outer join '.$send_log.' ');
+        $query->push('on '.$customers.'.id='.$send_log.'.customer ');
+        $query->push('and '.$campaigns.'.id='.$send_log.'.campaign ');
+        $query->push('where '.$campaigns.'.id='.$target->get_id().' ');
+        $query->push('and '.$campaigns.'.test='.$customers.'.tester ');
+        
+        if ($target->type === campaign_type::email) {
+            $query->push('and '.$customers.'.email != "" ');
+        } elseif ($target->type === campaign_type::sms) {
+            $query->push('and '.$customers.'.phone_number != "" ');
+        }
+   
+        if ($sended === false) {
+            $query->push('and '.$send_log.'.id is NULL ');
+        } elseif ($sended === true) {
+            $query->push('and '.$send_log.'.id is not NULL ');
+        }
+
+        if ($target->group !== null and $target->group->has_id()) {
+            $query->push('and '.$customers.'.grouped=');
+            $query->push(strval($target->group->get_id().' '));
+        }
+
+        $result = $this->get_database()->get_var($query->get());
+        
+        if ($result === null) {
+            throw $this->get_exception('count');
+        }
+        
+        return intval($result);
+    }
+}

+ 102 - 0
templates/01-database_converter.php

@@ -0,0 +1,102 @@
+<?php
+
+namespace cx_newsletter;
+
+abstract class database_converter
+implements database_converter_interface {
+    public function __construct() {
+        $this->target = null;
+    }
+
+    protected function check_target() : void {
+        if ($this->target === null) {
+            throw $this->not_loaded_exception();
+        }
+
+        if (!$this->target->is_complete()) {
+            throw $this->not_complete_exception();
+        }
+    }
+
+    protected function not_complete_exception() : \exception {
+        $exception = "\n\n";
+        $exception .= 'Item in the builder is not complete.';    
+        $exception .= "\n\n";
+
+        return new \exception($exception);      
+    }
+
+    protected function check_all_exists(array $target, array $list) : void {
+        foreach ($list as $name) {
+            if (!array_key_exists($name, $target)) {
+                throw $this->not_found_exception($name);
+            }
+        }
+    }
+
+    protected function id_or_null(array $target, string $name = 'id') : ?int {
+        if (array_key_exists($name, $target)) {
+            return intval($target[$name]);
+        }
+
+        return null;
+    }
+
+    protected function not_loaded_exception() : \exception {
+        $exception = "\n\n";
+        $exception .= 'Any content had not being loaded to this converter.';
+        $exception .= "\n\n";
+
+        return new \exception($exception); 
+    }
+
+    protected function not_found_exception(string $name) : \exception {
+        $exception = "\n\n";
+        $exception .= 'Item with name "'.$name.'" not found.';
+        $exception .= "\n\n";
+
+        return new \exception($exception); 
+    }
+
+    public function load_object(object $target) : self {
+        $this->target = $target;
+
+        return $this;
+    }
+
+    public function get_object() : object {
+        return $this->target;
+    }
+
+    protected function is_loaded() : bool {
+        return $this->target !== null;
+    }
+
+    protected function string_from_date(?\datetime $what) : ?string {
+        if ($what === null) {
+            return null;
+        }
+
+        return $what->format(\datetimeinterface::RFC3339);
+    }
+
+    protected function date_or_null(?string $what) : ?\datetime {
+        if ($what === null or strtoupper($what) === 'NULL') {
+            return null;
+        }
+        
+        $time = \datetime::createfromformat(
+            'Y-m-d H:i:s',
+            $what
+        );
+        
+        if ($time === false) {
+            return null;
+        }
+        
+        return $time;
+    }
+
+    protected \exception $not_loaded_exception;
+    protected ?object $target;
+}

+ 90 - 0
templates/02-database_worker.php

@@ -0,0 +1,90 @@
+<?php
+
+namespace cx_newsletter;
+use \wpdb;
+
+abstract class database_worker {
+    public function __construct(
+        wpdb $database, 
+        table_names $tables, 
+    ) {
+        $this->database = $database;
+        $this->tables = $tables;
+    }
+
+    protected function parse_limit(?int $limit) : string {
+        if ($limit === null) {
+            return '';
+        }
+
+        return 'limit '.strval($limit).' ';
+    }
+
+    protected function parse_filters(
+        array $filters,
+        filter_type $type = filter_type::equal,
+        filter_glue $glue = filter_glue::all
+    ) : string {
+        if (empty($filters)) {
+            return '';
+        }
+
+        $result = new \cx_appengine\string_builder('where ');
+        
+        foreach ($filters as $name => $content) {
+            $result->push($name);
+
+            if ($content === null) {
+                $result->push(' is null');
+            } elseif (is_numeric($content)) {
+                $result->push($type->value.strval($content));
+            } elseif (is_string($content)) {
+                $result->push($type->value.'"'.$content.'"');
+            } else {
+                throw $this->get_exception('parse_filters');
+            }
+
+            $result->push($glue->value);
+        }       
+
+        return $result->cut(strlen($glue->value))->push(' ')->get();
+    }
+
+    protected function get_exception(string $operation) : \exception {
+        $exception = "\n\n";
+        $exception .= 'Can not make operation on database in class: ';
+        $exception .= get_class($this).'. Trying to make: '.$operation.'.'; 
+        $exception .= "\n\n";
+
+        return new \exception($exception);
+    }
+
+    protected function get_has_id_exception() : \exception {
+        $exception = "\n\n";
+        $exception .= 'Can not make operation on database in class: ';
+        $exception .= get_class($this).'. Trying to insert item with ID.'; 
+        $exception .= "\n\n";
+
+        return new \exception($exception);
+    }
+
+    protected function get_blank_id_exception() : \exception {
+        $exception = "\n\n";
+        $exception .= 'Can not make operation on database in class: ';
+        $exception .= get_class($this).'. Trying to load target without ID.';
+        $exception .= "\n\n";
+
+        return new \exception($exception);
+    }
+
+    protected function get_tables() : table_names {
+        return $this->tables;
+    }
+
+    protected function get_database() : wpdb {
+        return $this->database;
+    }
+
+    private table_names $tables;
+    private wpdb $database;
+}

+ 32 - 0
templates/03-database_item.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace cx_newsletter;
+
+abstract class database_item 
+implements database_item_interface {
+    public function __construct(?int $id = null) {
+        $this->id = $id;
+    }
+
+    public function has_id() : bool {
+        return $this->id !== null;
+    }
+
+    public function get_id() : int {
+        if (!$this->has_id()) {
+            $exception = "\n\n";
+            $exception .= 'Database item which ID trying to be loaded, has ';
+            $exception .= 'no ID. That mean it could not exists, in the ';
+            $exception .= 'database, because only items which not exists in ';
+            $exception .= 'the database has not own ID.';
+            $exception .= "\n\n";
+
+            throw new \exception($exception);
+        }
+        
+        return $this->id;
+    }
+
+
+    private ?int $id;
+}   

+ 85 - 0
templates/04-database_builder.php

@@ -0,0 +1,85 @@
+<?php
+
+namespace cx_newsletter;
+use \wpdb;
+use \cx_appengine\string_builder;
+
+abstract class database_builder
+extends database_worker
+implements database_builder_interface {
+    public function __construct(
+        wpdb $database, 
+        table_names $tables, 
+        database_versions_manager $versions
+    ) {
+        parent::__construct($database, $tables);
+        $this->versions = $versions;
+    }
+
+    protected function drop_table(string $name) : void {
+        $query = $this->get_database()->prepare('drop table if exists '.$name);
+        $this->get_database()->query($query);
+    }
+
+    protected function table_exists(string $name) : bool {
+        $name = $this->get_database()->esc_like($name);
+        $query = $this->get_database()->prepare('show tables like %s', $name);
+
+        return $this->get_database()->get_var($query) === $name; 
+    }
+   
+    protected function add_column(
+        string $table, 
+        string $column, 
+        string $type
+    ) : void {
+        $query = (
+            'alter table '.$table.' '.
+            'add column '.$column.' '.$type
+        );
+
+        $this->get_database()->query($query);
+    }
+
+    protected function add_foreign_key(
+        string $table,
+        string $column,
+        string $foreign_table,
+        string $foreign_column
+    ) : void {
+        $query = (
+            'alter table '.$table.' '.
+            'add foreign key ('.$column.') '.
+            'references '.$foreign_table.'('.$foreign_column.')'
+        );
+
+        $this->get_database()->query($query);
+    }
+
+    protected function create_table(string $name, array $structure) : void {
+        $query = 'create table '.$name.' ';
+        $query .= $this->string_from_structure($structure);
+
+        $this->get_database()->query($query);
+    }
+
+    private function string_from_structure(array $structure) : string {
+        $flat = [];
+
+        foreach ($structure as $key => $type) {
+            array_push($flat, $key.' '.$type);
+        }
+
+        $result = new string_builder($flat, ', ');
+        $result->push(')');
+        $result->push_start('(');
+
+        return $result->get();
+    }
+
+    protected function get_versions() : database_versions_manager {
+        return $this->versions;
+    }
+
+    private database_versions_manager $versions;
+}

+ 9 - 0
templates/05-mapper.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace cx_newsletter;
+
+abstract class mapper
+extends database_worker
+implements mapper_interface {
+
+}

+ 23 - 0
templates/06-table_names_generator.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace cx_newsletter;
+
+abstract class table_names_generator {
+    public function __construct(\wpdb $database) {
+        $this->database = $database;
+    }
+ 
+    protected function add_prefixes(string $name) : string {
+        return $this->get_wordpress_prefix().$this->get_own_prefix().$name;
+    }
+
+    protected function get_own_prefix() : string {
+        return 'cx_newsletter_';
+    }
+
+    protected function get_wordpress_prefix() : string {
+        return $this->database->prefix;
+    }
+
+    private \wpdb $database;
+}

+ 24 - 0
templates/07-enum_converter.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace cx_newsletter;
+
+abstract class enum_converter 
+implements enum_converter_interface {
+    public function __construct() { 
+
+    }
+
+    public function load_enum(mixed $target) : self {
+        $this->target = $target;
+        return $this;
+    }
+
+    public function get_enum() : mixed {
+        return $this->target;
+    }
+
+    public abstract function load_string(string $target) : self;
+    public abstract function get_string() : string;
+
+    protected mixed $target;
+}

+ 73 - 0
templates/08-view.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace cx_newsletter;
+
+abstract class view 
+implements view_interface {
+    public function __construct(
+        \wpdb $database,
+        settings $settings,
+        \cx_appengine\templates $templates,
+        table_names $tables
+    ) {
+        $this->database = $database;
+        $this->settings = $settings;
+        $this->templates = $templates;
+        $this->tables = $tables;
+        $this->view = new \cx_appengine\view($_POST);
+    }
+
+    protected function get_settings() : settings {
+        return $this->settings;
+    }
+
+    protected function get_database() : \wpdb {
+        return $this->database;
+    }
+
+    protected function get_templates() : \cx_appengine\templates {
+        return $this->templates;
+    }
+
+    protected function get_view() : \cx_appengine\view {
+        return $this->view;
+    }
+
+    protected function get_tables() : table_names {
+        return $this->tables;
+    }
+
+    protected function add_activity(string $name) : self {
+        $activity = new $name(
+            $this->get_database(),
+            $this->get_settings(),
+            $this->get_templates(),
+            $this->get_tables()
+        );
+
+        $this->get_view()->add_activity($activity);
+
+        return $this;
+    }
+
+    public function action() : void {
+        $this->process();
+
+        echo(
+            $this
+            ->get_view()
+            ->prepare()
+            ->choose()
+            ->validate()
+            ->process()
+            ->render()
+            ->get()
+        );
+    }
+
+    private \cx_appengine\view $view;    
+    private \wpdb $database;
+    private settings $settings;
+    private table_names $tables;
+    private \cx_appengine\templates $templates;
+}

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است