Initial commit
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
/**
|
||||
* Activity Log Model - DB Model for Activity Logs
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class ActivityLog extends Model
|
||||
{
|
||||
protected $table = 'fc_activity_logs';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $fillable = [
|
||||
'object_type',
|
||||
'object_id',
|
||||
'action',
|
||||
'source',
|
||||
'description',
|
||||
'activity_by',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
// Ensure the key is added to every serialized row
|
||||
protected $appends = ['activity_by_email'];
|
||||
|
||||
// Cast the description column to an array (or object)
|
||||
protected $casts = [
|
||||
'description' => 'array', // Automatically decodes JSON to array
|
||||
// Use 'object' instead of 'array' if you prefer stdClass objects
|
||||
];
|
||||
|
||||
public static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($model) {
|
||||
if (empty($model->created_at)) {
|
||||
$model->created_at = fluentCrmTimestamp();
|
||||
}
|
||||
|
||||
if (empty($model->activity_by)) {
|
||||
$model->activity_by = 0;
|
||||
}
|
||||
|
||||
$model->updated_at = fluentCrmTimestamp();
|
||||
});
|
||||
|
||||
static::updated(function ($model) {
|
||||
$model->updated_at = fluentCrmTimestamp();
|
||||
});
|
||||
}
|
||||
|
||||
public function getActivityByEmailAttribute()
|
||||
{
|
||||
$user = User::where('ID', $this->activity_by)->first();
|
||||
if (!$user) {
|
||||
return null;
|
||||
}
|
||||
return $user->display_name . ' (' . $user->user_email . ')';
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,667 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
use FluentCrm\App\Services\BlockParser;
|
||||
use FluentCrm\App\Services\Helper;
|
||||
use FluentCrm\Framework\Support\Arr;
|
||||
use FluentCrm\Framework\Support\Str;
|
||||
|
||||
/**
|
||||
* CampaignEmail Model - DB Model for Campaign Emails
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class CampaignEmail extends Model
|
||||
{
|
||||
protected $table = 'fc_campaign_emails';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $appends = ['email_type_label'];
|
||||
|
||||
/**
|
||||
* Define the canonical email types and any legacy or module-specific aliases.
|
||||
*
|
||||
* @return array<string, array<int, string>>
|
||||
*/
|
||||
public static function getEmailTypeAliases()
|
||||
{
|
||||
return [
|
||||
'funnel_email_campaign' => ['funnel_email_campaign', 'automation'],
|
||||
'recurring_campaign' => ['recurring_campaign', 'recurring_email_campaign'],
|
||||
'custom_email_campaign' => ['custom_email_campaign', 'custom_email'],
|
||||
'campaign' => ['campaign'],
|
||||
'sequence' => ['sequence', 'email_sequence', 'sequence_email'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Map canonical email types to the user-facing labels used in reporting.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function getEmailTypeLabels()
|
||||
{
|
||||
return [
|
||||
'funnel_email_campaign' => __('Automation', 'fluent-crm'),
|
||||
'recurring_campaign' => __('Recurring Campaign', 'fluent-crm'),
|
||||
'custom_email_campaign' => __('Custom Email', 'fluent-crm'),
|
||||
'campaign' => __('Campaign', 'fluent-crm'),
|
||||
'sequence' => __('Sequence', 'fluent-crm'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the canonical email type key for filtering and reporting.
|
||||
*
|
||||
* @param string|null $emailType
|
||||
* @return string
|
||||
*/
|
||||
public static function normalizeEmailType($emailType)
|
||||
{
|
||||
$emailType = sanitize_text_field((string)$emailType);
|
||||
|
||||
foreach (static::getEmailTypeAliases() as $canonicalType => $aliases) {
|
||||
if (in_array($emailType, $aliases, true)) {
|
||||
return $canonicalType;
|
||||
}
|
||||
}
|
||||
|
||||
return $emailType ?: 'campaign';
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand selected canonical types into the raw slugs stored in email rows.
|
||||
*
|
||||
* @param array<int, string> $selectedTypes
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function expandEmailTypes(array $selectedTypes)
|
||||
{
|
||||
$expandedTypes = [];
|
||||
$aliases = static::getEmailTypeAliases();
|
||||
|
||||
foreach ($selectedTypes as $selectedType) {
|
||||
$canonicalType = static::normalizeEmailType($selectedType);
|
||||
$expandedTypes = array_merge($expandedTypes, $aliases[$canonicalType] ?? [$canonicalType]);
|
||||
}
|
||||
|
||||
return array_values(array_unique($expandedTypes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the human-readable email type label for reports and tables.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEmailTypeLabelAttribute()
|
||||
{
|
||||
return static::resolveEmailTypeLabel($this->email_type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a stored email type slug into a stable UI label.
|
||||
*
|
||||
* @param string|null $emailType
|
||||
* @return string
|
||||
*/
|
||||
public static function resolveEmailTypeLabel($emailType)
|
||||
{
|
||||
$emailType = static::normalizeEmailType($emailType);
|
||||
$labels = static::getEmailTypeLabels();
|
||||
|
||||
if (isset($labels[$emailType])) {
|
||||
return $labels[$emailType];
|
||||
}
|
||||
|
||||
return ucwords(str_replace(['_', '-'], ' ', $emailType ?: 'campaign'));
|
||||
}
|
||||
|
||||
/**
|
||||
* One2One: CampaignEmail belongs to one Campaign
|
||||
* @return Model
|
||||
*/
|
||||
public function campaign()
|
||||
{
|
||||
return $this->belongsTo(
|
||||
__NAMESPACE__ . '\Campaign', 'campaign_id', 'id'
|
||||
)->withoutGlobalScope('type');
|
||||
}
|
||||
|
||||
/**
|
||||
* One2One: CampaignEmail belongs to one Subscriber
|
||||
* @return Model
|
||||
*/
|
||||
public function subscriber()
|
||||
{
|
||||
return $this->belongsTo(
|
||||
__NAMESPACE__ . '\Subscriber', 'subscriber_id', 'id'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* One2One: CampaignEmail belongs to one Subject
|
||||
*
|
||||
* Note: The email_subject_id will be inserted by calculating the prioroty
|
||||
* from subjects table where the subjects are related to a parent Campaign.
|
||||
* So, when creating a campaign email, there will be an option to select a
|
||||
* subject from a list and that list will contain subjects related to the
|
||||
* parent campaign because a campaign can have many subjects and the campaign
|
||||
* email will get only one from that list by calculating the priority from subjects.
|
||||
*
|
||||
* @return Model
|
||||
*/
|
||||
public function subject()
|
||||
{
|
||||
return $this->belongsTo(
|
||||
__NAMESPACE__ . '\Subject', 'email_subject_id', 'id'
|
||||
);
|
||||
}
|
||||
|
||||
public function markAs($status)
|
||||
{
|
||||
$this->status = $status;
|
||||
$this->save();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function markAsSent($status = 'sent')
|
||||
{
|
||||
return $this->markAs($status);
|
||||
}
|
||||
|
||||
public function markAsFailed($status = 'failed')
|
||||
{
|
||||
return $this->markAs($status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data for the email to be sent
|
||||
* @return array
|
||||
*/
|
||||
public function data()
|
||||
{
|
||||
$email_subject = $this->getEmailSubject();
|
||||
$email_body = $this->getEmailBody();
|
||||
$headers = Helper::getMailHeader($this->email_headers);
|
||||
|
||||
return [
|
||||
'to' => [
|
||||
'email' => $this->email_address,
|
||||
'name' => ($this->subscriber) ? $this->subscriber->full_name : ''
|
||||
],
|
||||
'headers' => $headers,
|
||||
'subject' => $email_subject,
|
||||
'body' => $email_body,
|
||||
'campaign_id' => $this->campaign_id,
|
||||
'id' => $this->id,
|
||||
'subscriber_id' => $this->subscriber_id
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build preview data for one queued/sent email row.
|
||||
*
|
||||
* Route: GET /campaigns/emails/{email_id}/preview via CampaignController::previewEmail().
|
||||
* This is the contact/campaign/all-emails history preview, not the draft editor preview
|
||||
* route (POST /campaigns/email-preview-html). Keep the body rendering rules aligned with
|
||||
* CampaignController::getEmailPreviewBody(): raw/classic-builder templates must bypass
|
||||
* BlockParser, while block-editor templates should continue through BlockParser.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function previewData()
|
||||
{
|
||||
$emailSettings = fluentcrmGetGlobalSettings('email_settings', []);
|
||||
|
||||
$campaign = $this->campaign;
|
||||
$subscriber = $this->subscriber;
|
||||
|
||||
$emailBody = ($this->email_body) ? $this->email_body : (($campaign) ? $campaign->email_body : '');
|
||||
|
||||
$designTemplate = ($campaign) ? $campaign->design_template : '';
|
||||
|
||||
if (!$designTemplate) {
|
||||
$designTemplate = 'plain';
|
||||
}
|
||||
|
||||
$rawTemplates = [
|
||||
'raw_html',
|
||||
'visual_builder',
|
||||
'raw_classic'
|
||||
];
|
||||
|
||||
if (in_array($designTemplate, $rawTemplates, true) || ($this->is_parsed && $this->email_body)) {
|
||||
$emailBody = wp_unslash($emailBody);
|
||||
} else {
|
||||
$emailBody = (new BlockParser($subscriber))->parse($emailBody);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the campaign email body content text.
|
||||
*
|
||||
* This filter allows you to modify the email body content before it is sent to the subscriber.
|
||||
*
|
||||
* @param string $emailBody The email body content.
|
||||
* @param object $this->subscriber The subscriber object.
|
||||
* @since 2.7.0
|
||||
*
|
||||
*/
|
||||
$emailBody = apply_filters('fluent_crm/parse_campaign_email_text', $emailBody, $subscriber);
|
||||
|
||||
$templateConfig = wp_parse_args(
|
||||
($campaign) ? Arr::get($campaign->settings, 'template_config', []) : [],
|
||||
Helper::getTemplateConfig($designTemplate)
|
||||
);
|
||||
|
||||
$emailFooterConfig = ($campaign) ? Helper::getFooterConfig($campaign) : [];
|
||||
$footerText = Arr::get($emailFooterConfig, 'footer_content', '');
|
||||
|
||||
if ($subscriber) {
|
||||
$subscriber->campaign_id = $this->campaign_id;
|
||||
/**
|
||||
* Determine the footer text of a campaign email for previewing.
|
||||
*
|
||||
* This filter allows you to modify the footer text of a campaign email before it is sent to the subscriber.
|
||||
*
|
||||
* @param string $footerText The footer text of the campaign email.
|
||||
* @param object $subscriber The subscriber object.
|
||||
* @since 2.7.0
|
||||
*
|
||||
*/
|
||||
$footerText = apply_filters('fluent_crm/parse_campaign_email_text', $footerText, $subscriber);
|
||||
}
|
||||
|
||||
$preHeader = ($campaign) ? $campaign->email_pre_header : '';
|
||||
|
||||
if ($preHeader && $subscriber) {
|
||||
/**
|
||||
* Filter the pre-header text of a campaign email for previewing.
|
||||
*
|
||||
* @param string $preHeader The pre-header text of the campaign email.
|
||||
* @param object $subscriber The subscriber object.
|
||||
* @since 2.7.0
|
||||
*
|
||||
*/
|
||||
$preHeader = apply_filters('fluent_crm/parse_campaign_email_text', $preHeader, $subscriber);
|
||||
}
|
||||
|
||||
$emailFooterConfig['footer_content'] = $footerText;
|
||||
|
||||
/**
|
||||
* Filter the email body content using a specific email design template.
|
||||
*
|
||||
* This filter allows customization of the email body content by applying a specific design template.
|
||||
*
|
||||
* @param string $emailBody The original email body content before applying the design template.
|
||||
* @param array {
|
||||
* Contextual information for the email design template.
|
||||
*
|
||||
* @type string $preHeader The pre-header text for the email, if available.
|
||||
* @type string $email_body The original email body content.
|
||||
* @type string $footer_text The footer text for the email, if any.
|
||||
* @type array $config Configuration settings for the email template.
|
||||
* }
|
||||
* @param object|null $this->campaign The campaign object, if available.
|
||||
* @param object|null $this->subscriber The subscriber object, if available.
|
||||
* @since 1.0.0
|
||||
*
|
||||
*/
|
||||
$email_body = apply_filters(
|
||||
'fluent_crm/email-design-template-' . $designTemplate,
|
||||
$emailBody,
|
||||
[
|
||||
'preHeader' => $preHeader,
|
||||
'email_body' => $emailBody,
|
||||
'footer_text' => $footerText,
|
||||
'footer_config' => $emailFooterConfig,
|
||||
'config' => $templateConfig
|
||||
],
|
||||
$campaign,
|
||||
$subscriber
|
||||
);
|
||||
|
||||
|
||||
if (Str::contains($email_body, ['##crm.', '{{crm.'])) {
|
||||
/**
|
||||
* Filter the email body content for a campaign email and parse SmartCodes.
|
||||
*
|
||||
* This filter allows customization of the email body content before it is sent to the subscriber. There are FluentCRM-specific SmartCodes.
|
||||
*
|
||||
* @param string $email_body The email body content to be filtered.
|
||||
* @param object $this ->subscriber The subscriber object containing subscriber details.
|
||||
* @since 2.7.0
|
||||
*
|
||||
*/
|
||||
$email_body = apply_filters('fluent_crm/parse_extended_crm_text', $email_body, $subscriber);
|
||||
}
|
||||
|
||||
$preViewUrl = site_url('?fluentcrm=1&route=email_preview&_e_hash=' . $this->email_hash);
|
||||
$email_body = str_replace(['##web_preview_url##', '{{crm_global_email_footer}}', '{{crm_preheader_text}}'], [$preViewUrl, $footerText, $preHeader], $email_body);
|
||||
|
||||
|
||||
$email_body = str_replace(['https://fonts.googleapis.com/css2', 'https://fonts.googleapis.com/css'], 'https://fonts.bunny.net/css', $email_body);
|
||||
|
||||
return [
|
||||
'to' => [
|
||||
'email' => $this->email_address,
|
||||
'name' => ($subscriber) ? $subscriber->full_name : ''
|
||||
],
|
||||
'from' => [
|
||||
'name' => Arr::get($emailSettings, 'from_name'),
|
||||
'email' => Arr::get($emailSettings, 'from_email')
|
||||
],
|
||||
'reply' => null,
|
||||
'subject' => $this->email_subject,
|
||||
'body' => $email_body,
|
||||
'campaign_id' => $this->campaign_id,
|
||||
'id' => $this->id,
|
||||
'subscriber_id' => $this->subscriber_id
|
||||
];
|
||||
}
|
||||
|
||||
public function getEmailSubject()
|
||||
{
|
||||
return $this->email_subject;
|
||||
}
|
||||
|
||||
public function getEmailBody()
|
||||
{
|
||||
$subscriber = $this->subscriber;
|
||||
|
||||
if ($subscriber) {
|
||||
$subscriber->email_id = $this->id;
|
||||
}
|
||||
|
||||
$designTemplate = 'classic';
|
||||
|
||||
$campaign = $this->campaign;
|
||||
|
||||
if ($campaign) {
|
||||
$designTemplate = $campaign->design_template;
|
||||
}
|
||||
|
||||
if ($this->is_parsed && !$this->email_body && $campaign && $campaign->email_body) {
|
||||
// Recover unsent queue rows that were marked parsed after their body was cleared.
|
||||
$this->is_parsed = 0;
|
||||
}
|
||||
|
||||
if (!$this->is_parsed) {
|
||||
$rawTemplates = [
|
||||
'raw_html',
|
||||
'visual_builder',
|
||||
'raw_classic'
|
||||
];
|
||||
|
||||
$emailBody = ($campaign) ? $campaign->email_body : $this->email_body;
|
||||
|
||||
// Don't cache URL map if body has conditional blocks or merge tags inside href URLs
|
||||
$canCache = !Helper::hasConditionOnString($emailBody)
|
||||
&& !preg_match('/href=["\'][^"\']*\{\{/', $emailBody);
|
||||
|
||||
if (in_array($designTemplate, $rawTemplates)) {
|
||||
$emailBody = $this->campaign->email_body;
|
||||
} else {
|
||||
$emailBody = $this->getParsedEmailBody();
|
||||
}
|
||||
|
||||
$emailBody = str_replace(['https://fonts.googleapis.com/css2', 'https://fonts.googleapis.com/css'], 'https://fonts.bunny.net/css', $emailBody);
|
||||
|
||||
$emailBody = apply_filters('fluent_crm/parse_campaign_email_text', $emailBody, $subscriber);
|
||||
|
||||
$emailBody = apply_filters('fluentcrm_email_body_text', $emailBody, $subscriber, $this);
|
||||
|
||||
if ($campaign && $trackingType = $campaign->getClickTrackingStatus()) {
|
||||
$campaignUrls = $this->getCampaignUrls($emailBody, $canCache);
|
||||
if ($campaignUrls) {
|
||||
if ($trackingType === 'anonymous') {
|
||||
$emailBody = Helper::attachAnonymousUrls($emailBody, $campaignUrls, $this->id, $this->email_hash);
|
||||
} else {
|
||||
$emailBody = Helper::attachUrls($emailBody, $campaignUrls, $this->id, $this->email_hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->email_body = $emailBody;
|
||||
$this->is_parsed = 1;
|
||||
// Not saved to DB here — the parsed body is kept in memory for Mailer::send().
|
||||
// BaseHandler's mark-as-sent UPDATE persists is_parsed=1 and clears email_body.
|
||||
// On rare retry (process crash), the email re-parses from campaign body which
|
||||
// is correct. This avoids a ~15ms LONGTEXT write per email during bulk sends.
|
||||
}
|
||||
|
||||
$emailFooterConfig = [];
|
||||
$footerText = '';
|
||||
if ($subscriber) {
|
||||
$subscriber->campaign_id = $this->campaign_id;
|
||||
|
||||
static $footerConfigCache = [];
|
||||
$cacheKey = $this->campaign_id ?: 0;
|
||||
if (isset($footerConfigCache[$cacheKey])) {
|
||||
$emailFooterConfig = $footerConfigCache[$cacheKey];
|
||||
} else {
|
||||
$emailFooterConfig = Helper::getFooterConfig($campaign);
|
||||
$footerConfigCache[$cacheKey] = $emailFooterConfig;
|
||||
}
|
||||
$footerText = Arr::get($emailFooterConfig, 'footer_content', '');
|
||||
|
||||
if ($footerText) {
|
||||
/**
|
||||
* Filter the footer text of the campaign email.
|
||||
*
|
||||
* This filter allows you to modify the footer text of the campaign email before it is sent to the subscriber.
|
||||
*
|
||||
* @param string $footerText The footer text of the campaign email.
|
||||
* @param object $subscriber The subscriber object.
|
||||
* @since 2.7.0
|
||||
*
|
||||
*/
|
||||
$footerText = apply_filters('fluent_crm/parse_campaign_email_text', $footerText, $subscriber);
|
||||
|
||||
$preViewUrl = site_url('?fluentcrm=1&route=email_preview&_e_hash=' . $this->email_hash);
|
||||
$footerText = str_replace('##web_preview_url##', $preViewUrl, $footerText);
|
||||
$emailFooterConfig['footer_content'] = $footerText;
|
||||
}
|
||||
}
|
||||
|
||||
static $templateConfigCache = [];
|
||||
$templateCacheKey = $this->campaign_id ?: 0;
|
||||
if (isset($templateConfigCache[$templateCacheKey])) {
|
||||
$templateConfig = $templateConfigCache[$templateCacheKey];
|
||||
} else {
|
||||
if ($this->campaign && Arr::get($this->campaign->settings, 'template_config')) {
|
||||
$templateConfig = wp_parse_args($this->campaign->settings['template_config'], Helper::getTemplateConfig($this->campaign->design_template));
|
||||
} else {
|
||||
$templateConfig = Helper::getTemplateConfig();
|
||||
}
|
||||
$templateConfigCache[$templateCacheKey] = $templateConfig;
|
||||
}
|
||||
|
||||
$preHeader = ($this->campaign) ? $this->campaign->email_pre_header : '';
|
||||
|
||||
if ($preHeader && $subscriber) {
|
||||
/**
|
||||
* Filter the pre-header text of a campaign email.
|
||||
*
|
||||
* This filter allows you to modify the pre-header text of a campaign email before it is sent.
|
||||
*
|
||||
* @param string $preHeader The pre-header text of the campaign email.
|
||||
* @param object $subscriber The subscriber object containing subscriber details.
|
||||
* @since 2.7.0
|
||||
*
|
||||
*/
|
||||
$preHeader = apply_filters('fluent_crm/parse_campaign_email_text', $preHeader, $subscriber);
|
||||
}
|
||||
|
||||
$footerUrls = $this->getCampaignUrls($footerText, false);
|
||||
|
||||
if ($footerUrls) {
|
||||
$trackingType = $campaign ? $campaign->getClickTrackingStatus() : null;
|
||||
if ($trackingType === 'anonymous') {
|
||||
$footerText = Helper::attachAnonymousUrls($footerText, $footerUrls, $this->id, $this->email_hash);
|
||||
} else {
|
||||
$footerText = Helper::attachUrls($footerText, $footerUrls, $this->id, $this->email_hash);
|
||||
}
|
||||
}
|
||||
|
||||
$templateData = [
|
||||
'preHeader' => $preHeader,
|
||||
'email_body' => $this->email_body,
|
||||
'footer_text' => $footerText,
|
||||
'config' => $templateConfig,
|
||||
'footer_config' => $emailFooterConfig,
|
||||
];
|
||||
|
||||
/**
|
||||
* Filter the email design template content.
|
||||
*
|
||||
* This filter allows customization of the email design template content based on the template type.
|
||||
*
|
||||
* @param string $this ->email_body The original email body content.
|
||||
* @param array $templateData The data used for the template.
|
||||
* @param object $this ->campaign The campaign object.
|
||||
* @param object $this ->subscriber The subscriber object.
|
||||
* @since 1.0.0
|
||||
*
|
||||
*/
|
||||
$content = apply_filters(
|
||||
'fluent_crm/email-design-template-' . $designTemplate,
|
||||
$this->email_body,
|
||||
$templateData,
|
||||
$this->campaign,
|
||||
$this->subscriber
|
||||
);
|
||||
|
||||
$preViewUrl = site_url('?fluentcrm=1&route=email_preview&_e_hash=' . $this->email_hash);
|
||||
$content = str_replace(['##web_preview_url##', '{{crm_global_email_footer}}', '{{crm_preheader_text}}'], [$preViewUrl, $footerText, $preHeader], $content);
|
||||
|
||||
if (Str::contains($content, ['##crm.', '{{crm'])) {
|
||||
/**
|
||||
* Filter the content to parse extended CRM text such as SmartCodes.
|
||||
*
|
||||
* This filter allows you to modify the content by parsing extended CRM text. There are FluentCRM-specific SmartCodes available.
|
||||
*
|
||||
* @param string $content The content to be filtered.
|
||||
* @param object $subscriber The subscriber object.
|
||||
* @since 2.7.0
|
||||
*
|
||||
*/
|
||||
$content = apply_filters('fluent_crm/parse_extended_crm_text', $content, $subscriber);
|
||||
}
|
||||
|
||||
return Helper::injectTrackerPixel($content, $this->email_hash, $this->id);
|
||||
}
|
||||
|
||||
private function getParsedEmailBody()
|
||||
{
|
||||
if (!$this->campaign_id || !$this->campaign) {
|
||||
// return (new BlockParser($this->subscriber))->parse($this->email_body);
|
||||
return (new BlockParser($this->subscriber))->parse($this->email_body);
|
||||
}
|
||||
|
||||
static $parsedEmailBody = [];
|
||||
$originalBody = $this->campaign->email_body;
|
||||
|
||||
$hasConditions = Helper::hasConditionOnString($originalBody);
|
||||
|
||||
if (isset($parsedEmailBody[$this->campaign_id]) && !$hasConditions) {
|
||||
return $parsedEmailBody[$this->campaign_id];
|
||||
}
|
||||
|
||||
if ($this->campaign->status == 'archived' && !$hasConditions) {
|
||||
$cachedEmailBody = fluentcrm_get_campaign_meta($this->campaign_id, '_cached_email_body', true);
|
||||
if ($cachedEmailBody) {
|
||||
$parsedEmailBody[$this->campaign_id] = $cachedEmailBody;
|
||||
return $parsedEmailBody[$this->campaign_id];
|
||||
}
|
||||
}
|
||||
|
||||
$rawTemplates = [
|
||||
'raw_html',
|
||||
'visual_builder',
|
||||
'raw_classic'
|
||||
];
|
||||
|
||||
if (in_array($this->campaign->design_template, $rawTemplates)) {
|
||||
$emailBody = $originalBody;
|
||||
} else {
|
||||
// $emailBody = (new BlockParser($this->subscriber))->parse($originalBody);
|
||||
$emailBody = (new BlockParser($this->subscriber))->parse($originalBody);
|
||||
}
|
||||
|
||||
if ($hasConditions) {
|
||||
return $emailBody;
|
||||
}
|
||||
|
||||
$parsedEmailBody[$this->campaign_id] = $emailBody;
|
||||
return $emailBody;
|
||||
}
|
||||
|
||||
public function getCampaignUrls($emailBody, $cached = false)
|
||||
{
|
||||
$trackingType = fluentcrmTrackClicking();
|
||||
|
||||
if (!$trackingType) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!$cached || !$this->campaign_id) {
|
||||
return Helper::urlReplaces($emailBody);
|
||||
}
|
||||
|
||||
static $campaignUrls = [];
|
||||
if (isset($campaignUrls[$this->campaign_id])) {
|
||||
return $campaignUrls[$this->campaign_id];
|
||||
}
|
||||
|
||||
$campaignUrls[$this->campaign_id] = Helper::urlReplaces($emailBody);
|
||||
return $campaignUrls[$this->campaign_id];
|
||||
}
|
||||
|
||||
public function getClicks()
|
||||
{
|
||||
return fluentCrmDb()->table('fc_campaign_url_metrics')
|
||||
->select(['fc_campaign_url_metrics.counter', 'fc_url_stores.url', 'fc_campaign_url_metrics.id'])
|
||||
->where('type', 'click')
|
||||
->where('fc_campaign_url_metrics.subscriber_id', $this->subscriber_id)
|
||||
->where('fc_campaign_url_metrics.campaign_id', $this->campaign_id)
|
||||
->join('fc_url_stores', 'fc_url_stores.id', '=', 'fc_campaign_url_metrics.url_id')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function getSubjectCount($campaignId)
|
||||
{
|
||||
return static::select(
|
||||
'fc_campaign_emails.email_subject_id',
|
||||
fluentCrmDb()->raw('count(*) as total'),
|
||||
'fc_meta.value',
|
||||
'fc_meta.key'
|
||||
)
|
||||
->where('fc_campaign_emails.campaign_id', $campaignId)
|
||||
->groupBy('fc_campaign_emails.email_subject_id')
|
||||
->join('fc_meta', 'fc_meta.id', '=', 'fc_campaign_emails.email_subject_id')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function getOpenCount($subjectId)
|
||||
{
|
||||
return static::where('email_subject_id', $subjectId)
|
||||
->where('is_open', '>', 0)
|
||||
->count();
|
||||
}
|
||||
|
||||
public function setEmailHeadersAttribute($headers)
|
||||
{
|
||||
$this->attributes['email_headers'] = \maybe_serialize($headers);
|
||||
}
|
||||
|
||||
public function getEmailHeadersAttribute($settings)
|
||||
{
|
||||
return \maybe_unserialize($settings);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
use FluentCrm\App\Services\Helper;
|
||||
use FluentCrm\Framework\Support\Arr;
|
||||
|
||||
/**
|
||||
* CampaignUrlMetric Model - DB Model for Email URL Metrics
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class CampaignUrlMetric extends Model
|
||||
{
|
||||
protected $table = 'fc_campaign_url_metrics';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
public function campaign()
|
||||
{
|
||||
return $this->belongsTo(__NAMESPACE__ . '\Campaign', 'campaign_id', 'id')
|
||||
->withoutGlobalScope('type');
|
||||
}
|
||||
|
||||
public function subscriber()
|
||||
{
|
||||
return $this->belongsTo(__NAMESPACE__ . '\Subscriber', 'subscriber_id', 'id');
|
||||
}
|
||||
|
||||
public function url_stores()
|
||||
{
|
||||
return $this->belongsTo(__NAMESPACE__ . '\UrlStores', 'url_id', 'id');
|
||||
}
|
||||
|
||||
public static function maybeInsert($data)
|
||||
{
|
||||
$query = static::where([
|
||||
'campaign_id' => $data['campaign_id'],
|
||||
'subscriber_id' => $data['subscriber_id'],
|
||||
'type' => $data['type']
|
||||
])->when(!empty($data['url_id']), function ($query) use ($data) {
|
||||
return $query->where('url_id', $data['url_id']);
|
||||
});
|
||||
|
||||
if ($instance = $query->first()) {
|
||||
$instance->counter += 1;
|
||||
$instance->save();
|
||||
return $instance;
|
||||
}
|
||||
|
||||
return static::create($data);
|
||||
}
|
||||
|
||||
public function getLinksReport($campaign)
|
||||
{
|
||||
if (is_numeric($campaign)) {
|
||||
$campaign = Campaign::withoutGlobalScopes()->find($campaign);
|
||||
}
|
||||
|
||||
if (!$campaign) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$settings = $campaign->settings;
|
||||
|
||||
$clickTracker = $settings['click_tracker'] ?? true;
|
||||
|
||||
// is anonimous tracking enabled?
|
||||
if ($clickTracker === 'anonymous') {
|
||||
// get from meta
|
||||
$links = fluentcrm_get_campaign_meta($campaign->id, '_ano_url_clicks', true);
|
||||
$formattedLinks = [];
|
||||
if ($links && is_array($links)) {
|
||||
$index = 1;
|
||||
foreach ($links as $link => $count) {
|
||||
$formattedLinks[] = [
|
||||
'id' => $index,
|
||||
'url' => esc_url_raw($link),
|
||||
'total' => $count
|
||||
];
|
||||
$index++;
|
||||
}
|
||||
}
|
||||
|
||||
// sort by total desc
|
||||
usort($formattedLinks, function ($a, $b) {
|
||||
return $b['total'] <=> $a['total'];
|
||||
});
|
||||
|
||||
return $this->maybeTransformSmartLinks($formattedLinks);
|
||||
}
|
||||
|
||||
if ($clickTracker === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$stats = static::select(
|
||||
fluentCrmDb()->raw('count(*) as total'),
|
||||
'fc_url_stores.url',
|
||||
'fc_url_stores.id'
|
||||
)
|
||||
->where('fc_campaign_url_metrics.campaign_id', $campaign->id)
|
||||
->where('fc_campaign_url_metrics.type', 'click')
|
||||
->groupBy('fc_campaign_url_metrics.url_id')
|
||||
->join('fc_url_stores', 'fc_url_stores.id', '=', 'fc_campaign_url_metrics.url_id')
|
||||
->orderBy('total', 'DESC')
|
||||
->get()->toArray();
|
||||
|
||||
$formatedLinks = [];
|
||||
|
||||
foreach ($stats as $stat) {
|
||||
$url = str_replace(['&'], ['&'], $stat['url']);
|
||||
$url = esc_url_raw($url);
|
||||
|
||||
if (isset($formatedLinks[$url])) {
|
||||
$formatedLinks[$url]['total'] += $stat['total'];
|
||||
continue;
|
||||
}
|
||||
|
||||
$formatedLinks[$url] = [
|
||||
'id' => $stat['id'],
|
||||
'url' => $url,
|
||||
'total' => $stat['total']
|
||||
];
|
||||
}
|
||||
|
||||
$sortedLinks = array_values($formatedLinks);
|
||||
usort($sortedLinks, function ($a, $b) {
|
||||
return $b['total'] <=> $a['total'];
|
||||
});
|
||||
|
||||
return $this->maybeTransformSmartLinks($sortedLinks);
|
||||
}
|
||||
|
||||
public function getCampaignAnalytics($campaign)
|
||||
{
|
||||
if (is_numeric($campaign)) {
|
||||
$campaign = Campaign::withoutGlobalScopes()->find($campaign);
|
||||
}
|
||||
|
||||
if (!$campaign) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$unsubscribeCount = CampaignUrlMetric::where('campaign_id', $campaign->id)
|
||||
->where('type', 'unsubscribe')
|
||||
->distinct()
|
||||
->count('subscriber_id');
|
||||
|
||||
$formattedStatus = [];
|
||||
if ($campaign->getOpenTrackingStatus(false) === 'anonymous') {
|
||||
$openCount = fluentcrm_get_campaign_meta($campaign->id, '_ano_open_count', true);
|
||||
if (!$openCount) {
|
||||
$openCount = 0;
|
||||
}
|
||||
} else {
|
||||
$openCount = fluentCrmDb()->table('fc_campaign_emails')
|
||||
->where('campaign_id', $campaign->id)
|
||||
->where(function ($q) {
|
||||
$q->where('is_open', 1)
|
||||
->orWhereNotNull('click_counter');
|
||||
})
|
||||
->count();
|
||||
}
|
||||
|
||||
if ($campaign->getClickTrackingStatus(false) === 'anonymous') {
|
||||
$clicks = fluentcrm_get_campaign_meta($campaign->id, '_ano_url_clicks', true);
|
||||
$clickCount = 0;
|
||||
if ($clicks && is_array($clicks)) {
|
||||
$clickCount = array_sum($clicks);
|
||||
}
|
||||
} else {
|
||||
$clickCount = fluentCrmDb()->table('fc_campaign_emails')
|
||||
->where('campaign_id', $campaign->id)
|
||||
->whereNotNull('click_counter')
|
||||
->count();
|
||||
}
|
||||
|
||||
if ($openCount) {
|
||||
$formattedStatus['open'] = [
|
||||
'total' => $openCount,
|
||||
/* translators: %d: number of opens */
|
||||
'label' => sprintf(__('Open Rate (%d)', 'fluent-crm'), $openCount),
|
||||
'type' => 'open',
|
||||
'is_percent' => true,
|
||||
'icon_class' => 'dashicons dashicons-buddicons-pm'
|
||||
];
|
||||
}
|
||||
|
||||
if ($clickCount) {
|
||||
$formattedStatus['click'] = [
|
||||
'total' => $clickCount,
|
||||
/* translators: %d: number of clicks */
|
||||
'label' => sprintf(__('Click Rate (%d)', 'fluent-crm'), $clickCount),
|
||||
'type' => 'click',
|
||||
'is_percent' => true,
|
||||
'icon_class' => 'el-icon el-icon-position'
|
||||
];
|
||||
}
|
||||
|
||||
if ($openCount && $clickCount) {
|
||||
$formattedStatus['ctor'] = [
|
||||
'total' => number_format(($clickCount / $openCount) * 100, 2) . '%',
|
||||
'label' => __('Click To Open Rate', 'fluent-crm'),
|
||||
'type' => 'ctor',
|
||||
'icon_class' => 'el-icon el-icon-chat-dot-square'
|
||||
];
|
||||
}
|
||||
|
||||
if ($unsubscribeCount) {
|
||||
$formattedStatus['unsubscribe'] = [
|
||||
'total' => $unsubscribeCount,
|
||||
/* translators: %d: number of unsubscribes */
|
||||
'label' => sprintf(__('Unsubscribe (%d)', 'fluent-crm'), $unsubscribeCount),
|
||||
'type' => 'unsubscribe',
|
||||
'is_percent' => true,
|
||||
'icon_class' => 'el-icon el-icon-warning-outline'
|
||||
];
|
||||
}
|
||||
|
||||
$revenue = fluentcrm_get_campaign_meta($campaign->id, '_campaign_revenue');
|
||||
|
||||
if ($revenue && $revenue->value) {
|
||||
$data = (array)$revenue->value;
|
||||
foreach ($data as $currency => $cents) {
|
||||
if ($cents && $currency !== 'orderIds') {
|
||||
$formattedStatus['revenue'] = [
|
||||
'label' => __('Revenue', 'fluent-crm') . ' (' . $currency . ')',
|
||||
'type' => 'revenue',
|
||||
'total' => number_format($cents / 100, 2),
|
||||
'icon_class' => 'el-icon el-icon-money'
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $formattedStatus;
|
||||
}
|
||||
|
||||
public function getSubjectStats($campaign)
|
||||
{
|
||||
$subjects = $campaign->subjects()->get();
|
||||
|
||||
if ($subjects->isEmpty()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$subjectCounts = (new CampaignEmail)->getSubjectCount($campaign->id);
|
||||
|
||||
$totalClicks = 0;
|
||||
$totalOpens = 0;
|
||||
|
||||
foreach ($subjectCounts as $subjectCount) {
|
||||
$metric = $this->getSubjectMetric(
|
||||
$subjectCount->email_subject_id, $campaign->id
|
||||
);
|
||||
$totalClicks += $metric['total_clicks'];
|
||||
$totalOpens += $metric['total_opens'];
|
||||
$subjectCount->metric = $metric;
|
||||
}
|
||||
|
||||
return [
|
||||
'subjects' => $subjectCounts,
|
||||
'total_clicks' => $totalClicks,
|
||||
'total_opens' => $totalOpens
|
||||
];
|
||||
}
|
||||
|
||||
private function getSubjectMetric($subjectId, $campaignId)
|
||||
{
|
||||
$clickMetrics = $this->getClickMetrics($campaignId, $subjectId);
|
||||
|
||||
$openCount = (new CampaignEmail)->getOpenCount($subjectId);
|
||||
|
||||
$clickTotal = array_sum($clickMetrics->pluck('total')->toArray());
|
||||
|
||||
return [
|
||||
'clicks' => $clickMetrics,
|
||||
'total_clicks' => $clickTotal,
|
||||
'total_opens' => $openCount
|
||||
];
|
||||
}
|
||||
|
||||
public function getClickMetrics($campaignId, $subjectId)
|
||||
{
|
||||
return static::select(
|
||||
fluentCrmDb()->raw('count(*) as total'),
|
||||
'fc_url_stores.url'
|
||||
)
|
||||
->where('fc_campaign_url_metrics.campaign_id', $campaignId)
|
||||
->where('fc_campaign_url_metrics.type', 'click')
|
||||
->where('fc_campaign_emails.email_subject_id', $subjectId)
|
||||
->groupBy('fc_campaign_url_metrics.url_id')
|
||||
->join('fc_url_stores', 'fc_url_stores.id', '=', 'fc_campaign_url_metrics.url_id')
|
||||
->join('fc_campaign_emails', 'fc_campaign_emails.subscriber_id', '=', 'fc_campaign_url_metrics.subscriber_id')
|
||||
->orderBy('total', 'DESC')
|
||||
->get();
|
||||
}
|
||||
|
||||
private function maybeTransformSmartLinks($links)
|
||||
{
|
||||
if (!apply_filters('fluent_crm/has_smartlink', false)) {
|
||||
return $links;
|
||||
}
|
||||
|
||||
foreach ($links as $index => $link) {
|
||||
$url = $link['url'];
|
||||
if (strpos($url, 'route=smart_url&slug=') !== false) {
|
||||
// this is a smart-link
|
||||
$smartLink = apply_filters('fluent_crm/smartlink_by_short_url', null, $url);
|
||||
if ($smartLink) {
|
||||
$links[$index]['destination'] = $smartLink->target_url;
|
||||
$links[$index]['title'] = $smartLink->title;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $links;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
use FluentCrm\App\Models\Model;
|
||||
use FluentCrm\App\Models\Subscriber;
|
||||
use FluentCrm\Framework\Support\Arr;
|
||||
|
||||
class Company extends Model
|
||||
{
|
||||
protected $table = 'fc_companies';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $fillable = [
|
||||
'hash',
|
||||
'name',
|
||||
'owner_id',
|
||||
'industry',
|
||||
'type',
|
||||
'email',
|
||||
'phone',
|
||||
'address_line_1',
|
||||
'address_line_2',
|
||||
'postal_code',
|
||||
'city',
|
||||
'state',
|
||||
'country',
|
||||
'timezone',
|
||||
'employees_number',
|
||||
'description',
|
||||
'logo',
|
||||
'linkedin_url',
|
||||
'facebook_url',
|
||||
'twitter_url',
|
||||
'meta',
|
||||
'website',
|
||||
'date_of_start',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get subscriber mappable fields.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function mappables()
|
||||
{
|
||||
return [
|
||||
'name' => __('Company Name *', 'fluent-crm'),
|
||||
'owner_email' => __('Owner Email', 'fluent-crm'),
|
||||
'owner_name' => __('Owner Name', 'fluent-crm'),
|
||||
'industry' => __('Industry', 'fluent-crm'),
|
||||
'description' => __('Company Description', 'fluent-crm'),
|
||||
'logo' => __('Company Logo URL', 'fluent-crm'),
|
||||
'type' => __('Type', 'fluent-crm'),
|
||||
'email' => __('Company Email', 'fluent-crm'),
|
||||
'phone' => __('Company Phone', 'fluent-crm'),
|
||||
'address_line_1' => __('Address Line 1', 'fluent-crm'),
|
||||
'address_line_2' => __('Address Line 2', 'fluent-crm'),
|
||||
'postal_code' => __('Postal Code', 'fluent-crm'),
|
||||
'city' => __('City', 'fluent-crm'),
|
||||
'state' => __('State', 'fluent-crm'),
|
||||
'country' => __('Country', 'fluent-crm'),
|
||||
'employees_number' => __('Employees Number', 'fluent-crm'),
|
||||
'linkedin_url' => __('LinkedIn URL', 'fluent-crm'),
|
||||
'facebook_url' => __('Facebook URL', 'fluent-crm'),
|
||||
'twitter_url' => __('Twitter URL', 'fluent-crm'),
|
||||
'website' => __('Website URL', 'fluent-crm')
|
||||
];
|
||||
}
|
||||
|
||||
protected $searchable = [
|
||||
'name',
|
||||
'phone',
|
||||
'description',
|
||||
'email'
|
||||
];
|
||||
|
||||
public static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($model) {
|
||||
$model->hash = md5(wp_generate_uuid4() . '_' . time() . '_' . wp_rand(1000, 9999));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Local scope to filter companies by search/query string
|
||||
*/
|
||||
public function scopeSearchBy($query, $search)
|
||||
{
|
||||
if ($search) {
|
||||
$fields = $this->searchable;
|
||||
$query->where(function ($query) use ($fields, $search) {
|
||||
$query->where(array_shift($fields), 'LIKE', "%$search%");
|
||||
foreach ($fields as $field) {
|
||||
$query->orWhere($field, 'LIKE', "%$search%");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function scopeOfType($query, $status)
|
||||
{
|
||||
return $query->where('type', $status);
|
||||
}
|
||||
|
||||
public function scopeOfIndustry($query, $status)
|
||||
{
|
||||
return $query->where('industry', $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the subscribers that belongs to the company.
|
||||
*
|
||||
* @return \FluentCrm\Framework\Database\Orm\Relations\BelongsToMany
|
||||
*/
|
||||
public function subscribers()
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
__NAMESPACE__ . '\Subscriber', 'fc_subscriber_pivot', 'object_id', 'subscriber_id'
|
||||
)->where('object_type', __CLASS__);
|
||||
}
|
||||
|
||||
public function owner()
|
||||
{
|
||||
return $this->belongsTo(Subscriber::class, 'owner_id', 'id');
|
||||
}
|
||||
|
||||
public function getContactsCount()
|
||||
{
|
||||
return $this->subscribers()->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* A Company has many notes and activities.
|
||||
*
|
||||
* @return \FluentCrm\Framework\Database\Orm\Relations\HasMany
|
||||
*/
|
||||
public function notes()
|
||||
{
|
||||
return $this->hasMany(CompanyNote::class, 'subscriber_id', 'id');
|
||||
}
|
||||
|
||||
public function setMetaAttribute($meta)
|
||||
{
|
||||
$this->attributes['meta'] = \maybe_serialize($meta);
|
||||
}
|
||||
|
||||
public function getMetaAttribute($meta)
|
||||
{
|
||||
$metaData = \maybe_unserialize($meta);
|
||||
|
||||
if (!$metaData) {
|
||||
return [
|
||||
'custom_values' => []
|
||||
];
|
||||
}
|
||||
|
||||
$metaDefaults = [
|
||||
'custom_values' => []
|
||||
];
|
||||
|
||||
return array_merge($metaDefaults, $metaData);
|
||||
}
|
||||
|
||||
public function getCustomValues()
|
||||
{
|
||||
return Arr::get($this->meta, 'custom_values', []);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
/**
|
||||
* SubscriberNote Model - DB Model for Contact's notes
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
class CompanyNote extends Model
|
||||
{
|
||||
protected $table = 'fc_subscriber_notes';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $fillable = [
|
||||
'subscriber_id',
|
||||
'parent_id',
|
||||
'created_by',
|
||||
'type',
|
||||
'title',
|
||||
'description',
|
||||
'created_at'
|
||||
];
|
||||
|
||||
public static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($model) {
|
||||
if(empty($model->created_at)) {
|
||||
$model->created_at = fluentCrmTimestamp();
|
||||
}
|
||||
|
||||
$model->status = '_company_note_';
|
||||
|
||||
$model->updated_at = fluentCrmTimestamp();
|
||||
$model->created_by = $model->created_by ?: get_current_user_id();
|
||||
});
|
||||
|
||||
static::updated(function ($model) {
|
||||
$model->updated_at = fluentCrmTimestamp();
|
||||
});
|
||||
|
||||
static::addGlobalScope('status', function ($builder) {
|
||||
$builder->where('status', '_company_note_'); // This disguised the Company Note from SubscriberNote
|
||||
});
|
||||
}
|
||||
/**
|
||||
* One2One: CompanyNote belongs to one Company
|
||||
* @return \FluentCrm\Framework\Database\Orm\Relations\BelongsTo
|
||||
*/
|
||||
public function company()
|
||||
{
|
||||
return $this->belongsTo(
|
||||
__NAMESPACE__.'\Company', 'subscriber_id', 'id'
|
||||
);
|
||||
}
|
||||
|
||||
public function markAs($status)
|
||||
{
|
||||
$this->status = $status;
|
||||
$this->save();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function createdBy()
|
||||
{
|
||||
if(!$this->created_by) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$user = get_user_by('ID', $this->created_by);
|
||||
|
||||
if (!$user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
'ID' => $user->ID,
|
||||
'first_name' => $user->first_name,
|
||||
'last_name' => $user->last_name,
|
||||
'display_name' => $user->display_name
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
use FluentCrm\Framework\Support\Arr;
|
||||
|
||||
/**
|
||||
* CustomCompanyField Model - DB Model for Company Contact Fields
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 2.8.50
|
||||
*/
|
||||
class CustomCompanyField extends CustomContactField
|
||||
{
|
||||
protected $globalMetaName = 'company_custom_fields';
|
||||
|
||||
|
||||
public function getFieldGroups()
|
||||
{
|
||||
$fieldGroups = fluentcrm_get_option('company_field_groups');
|
||||
|
||||
if (!$fieldGroups) {
|
||||
$fieldGroups = [
|
||||
[
|
||||
'slug' => 'default',
|
||||
'title' => __('Custom Company Data', 'fluent-crm')
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
return $fieldGroups;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
use FluentCrm\Framework\Support\Arr;
|
||||
|
||||
/**
|
||||
* CustomContactField Model - DB Model for Custom Contact Fields
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class CustomContactField
|
||||
{
|
||||
protected $globalMetaName = 'contact_custom_fields';
|
||||
|
||||
public function getGlobalFields($with = [])
|
||||
{
|
||||
$data['fields'] = fluentcrm_get_option($this->globalMetaName, []);
|
||||
|
||||
if (in_array('field_types', $with)) {
|
||||
$data['field_types'] = $this->getFieldTypes();
|
||||
}
|
||||
|
||||
if (in_array('field_groups', $with)) {
|
||||
$data['field_groups'] = $this->getFieldGroups();
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getFieldTypes()
|
||||
{
|
||||
/**
|
||||
* Modify the global custom contact field types for FluentCRM custom contact fields.
|
||||
*
|
||||
* The default field types are: 'text', 'textarea', 'number', 'single-select', 'multi-select', 'radio', 'checkbox', 'date', 'date_time'.
|
||||
*
|
||||
* @since 2.7.0
|
||||
*
|
||||
* @param array {
|
||||
* An associative array of field types.
|
||||
*
|
||||
* @type array $text {
|
||||
* @type string $type The type of the field.
|
||||
* @type string $label The label for the field.
|
||||
* @type string $value_type The value type of the field.
|
||||
* }
|
||||
* @type array $textarea {
|
||||
* @type string $type The type of the field.
|
||||
* @type string $label The label for the field.
|
||||
* @type string $value_type The value type of the field.
|
||||
* }
|
||||
* @type array $number {
|
||||
* @type string $type The type of the field.
|
||||
* @type string $label The label for the field.
|
||||
* @type string $value_type The value type of the field.
|
||||
* }
|
||||
* @type array $single-select {
|
||||
* @type string $type The type of the field.
|
||||
* @type string $label The label for the field.
|
||||
* @type string $value_type The value type of the field.
|
||||
* }
|
||||
* @type array $multi-select {
|
||||
* @type string $type The type of the field.
|
||||
* @type string $label The label for the field.
|
||||
* @type string $value_type The value type of the field.
|
||||
* }
|
||||
* @type array $radio {
|
||||
* @type string $type The type of the field.
|
||||
* @type string $label The label for the field.
|
||||
* @type string $value_type The value type of the field.
|
||||
* }
|
||||
* @type array $checkbox {
|
||||
* @type string $type The type of the field.
|
||||
* @type string $label The label for the field.
|
||||
* @type string $value_type The value type of the field.
|
||||
* }
|
||||
* @type array $date {
|
||||
* @type string $type The type of the field.
|
||||
* @type string $label The label for the field.
|
||||
* @type string $value_type The value type of the field.
|
||||
* }
|
||||
* @type array $date_time {
|
||||
* @type string $type The type of the field.
|
||||
* @type string $label The label for the field.
|
||||
* @type string $value_type The value type of the field.
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
return apply_filters('fluent_crm/global_field_types', [
|
||||
'text' => [
|
||||
'type' => 'text',
|
||||
'label' => __('Single Line Text', 'fluent-crm'),
|
||||
'value_type' => 'string'
|
||||
],
|
||||
'textarea' => [
|
||||
'type' => 'textarea',
|
||||
'label' => __('Multi Line Text', 'fluent-crm'),
|
||||
'value_type' => 'string'
|
||||
],
|
||||
'number' => [
|
||||
'type' => 'number',
|
||||
'label' => __('Numeric Field', 'fluent-crm'),
|
||||
'value_type' => 'numeric'
|
||||
],
|
||||
'single-select' => [
|
||||
'type' => 'select-one',
|
||||
'label' => __('Select choice', 'fluent-crm'),
|
||||
'value_type' => 'string'
|
||||
],
|
||||
'multi-select' => [
|
||||
'type' => 'select-multi',
|
||||
'label' => __('Multiple Select choice', 'fluent-crm'),
|
||||
'value_type' => 'array'
|
||||
],
|
||||
'radio' => [
|
||||
'type' => 'radio',
|
||||
'label' => __('Radio Choice', 'fluent-crm'),
|
||||
'value_type' => 'string'
|
||||
],
|
||||
'checkbox' => [
|
||||
'type' => 'checkbox',
|
||||
'label' => __('Checkboxes', 'fluent-crm'),
|
||||
'value_type' => 'array'
|
||||
],
|
||||
'date' => [
|
||||
'type' => 'date',
|
||||
'label' => __('Date', 'fluent-crm'),
|
||||
'value_type' => 'date'
|
||||
],
|
||||
'date_time' => [
|
||||
'type' => 'date_time',
|
||||
'label' => __('Date and Time', 'fluent-crm'),
|
||||
'value_type' => 'datetime'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function saveGlobalFields($fields)
|
||||
{
|
||||
$slugs = [];
|
||||
|
||||
foreach ($fields as $field) {
|
||||
if (isset($field['slug'])) {
|
||||
$slugs[] = $field['slug'];
|
||||
}
|
||||
}
|
||||
|
||||
$formattedFields = [];
|
||||
|
||||
$keys = [];
|
||||
foreach ($fields as $field) {
|
||||
|
||||
if (empty($field['slug'])) {
|
||||
$field['slug'] = $this->generateSlug($field, $slugs);
|
||||
}
|
||||
|
||||
if (in_array($field['slug'], $keys)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$keys[] = $field['slug'];
|
||||
|
||||
$formattedFields[] = $field;
|
||||
}
|
||||
|
||||
fluentcrm_update_option($this->globalMetaName, $formattedFields);
|
||||
|
||||
return $formattedFields;
|
||||
}
|
||||
|
||||
protected function generateSlug($field, $slugs)
|
||||
{
|
||||
$label = str_replace(' ', '_', $field['label']);
|
||||
$label = sanitize_title($label, 'custom_field', 'view');
|
||||
$label = substr($label, 0, 25);
|
||||
$originalLabel = $label;
|
||||
|
||||
if (is_numeric($label)) {
|
||||
$label = 'cf_' . $label;
|
||||
}
|
||||
|
||||
$mainColumns = array_merge(
|
||||
(new Subscriber)->getFillable(),
|
||||
['id', 'updated_at']
|
||||
);
|
||||
|
||||
if (in_array($label, $mainColumns)) {
|
||||
$label = 'cf_' . $label;
|
||||
}
|
||||
|
||||
$index = 1;
|
||||
|
||||
while (in_array($label, $slugs)) {
|
||||
$label = $originalLabel . '_' . $index;
|
||||
$index++;
|
||||
}
|
||||
|
||||
return $label;
|
||||
}
|
||||
|
||||
public function formatCustomFieldValues($values, $fields = [])
|
||||
{
|
||||
if (!$values) {
|
||||
return $values;
|
||||
}
|
||||
if (!$fields) {
|
||||
$rawFields = fluentcrm_get_option($this->globalMetaName, []);
|
||||
foreach ($rawFields as $field) {
|
||||
$fields[$field['slug']] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($values as $valueKey => $value) {
|
||||
|
||||
$isArrayType = Arr::get($fields, $valueKey . '.type') == 'checkbox' || Arr::get($fields, $valueKey . '.type') == 'select-multi';
|
||||
|
||||
if (!is_array($value) && $isArrayType) {
|
||||
$itemValues = explode(',', $value);
|
||||
$trimmedvalues = [];
|
||||
foreach ($itemValues as $itemValue) {
|
||||
$trimmedvalues[] = trim($itemValue);
|
||||
}
|
||||
if ($itemValue) {
|
||||
$values[$valueKey] = $trimmedvalues;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
public function getFieldGroups()
|
||||
{
|
||||
$fieldGroups = fluentcrm_get_option('contact_field_groups');
|
||||
|
||||
if (!$fieldGroups) {
|
||||
$fieldGroups = [
|
||||
[
|
||||
'slug' => 'default',
|
||||
'title' => __('Custom Profile Data', 'fluent-crm')
|
||||
]
|
||||
];
|
||||
}
|
||||
return $fieldGroups;
|
||||
}
|
||||
|
||||
public function updateGroupName($oldName, $newName)
|
||||
{
|
||||
$currentCustomFields = fluentcrm_get_option($this->globalMetaName);
|
||||
|
||||
$updatedCustomFields = [];
|
||||
|
||||
foreach ($currentCustomFields as $customField) {
|
||||
if (isset($customField['group']) && $customField['group'] == $oldName) {
|
||||
$customField['group'] = $newName;
|
||||
}
|
||||
$updatedCustomFields[] = $customField;
|
||||
}
|
||||
|
||||
fluentcrm_update_option($this->globalMetaName, $updatedCustomFields);
|
||||
|
||||
return $updatedCustomFields;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
use FluentCrm\App\Services\Helper;
|
||||
|
||||
/**
|
||||
* CustomEmailCampaign Model - DB Model for Custom Emails
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
class CustomEmailCampaign extends Campaign
|
||||
{
|
||||
protected static $type = 'custom_email_campaign';
|
||||
|
||||
public static function getMock()
|
||||
{
|
||||
$defaultTemplate = Helper::getDefaultEmailTemplate();
|
||||
return [
|
||||
'id' => '',
|
||||
'title' => __('Custom Email', 'fluent-crm'),
|
||||
'status' => 'published',
|
||||
'template_id' => '',
|
||||
'email_subject' => '',
|
||||
'email_pre_header' => '',
|
||||
'email_body' => '',
|
||||
'utm_status' => 0,
|
||||
'utm_source' => '',
|
||||
'utm_medium' => '',
|
||||
'utm_campaign' => '',
|
||||
'utm_term' => '',
|
||||
'utm_content' => '',
|
||||
'design_template' => $defaultTemplate,
|
||||
'settings' => (object)[
|
||||
'template_config' => Helper::getTemplateConfig($defaultTemplate)
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
/**
|
||||
* SubscriberNote Model - DB Model for Contact's notes
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class EventTracker extends Model
|
||||
{
|
||||
protected $table = 'fc_event_tracking';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $fillable = [
|
||||
'subscriber_id',
|
||||
'counter',
|
||||
'created_by',
|
||||
'provider',
|
||||
'event_key',
|
||||
'title',
|
||||
'value'
|
||||
];
|
||||
|
||||
public static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($model) {
|
||||
$model->created_by = $model->created_by ?: get_current_user_id();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* One2One: SubscriberNote belongs to one Subscriber
|
||||
* @return \FluentCrm\Framework\Database\Orm\Relations\BelongsTo
|
||||
*/
|
||||
public function subscriber()
|
||||
{
|
||||
return $this->belongsTo(
|
||||
__NAMESPACE__ . '\Subscriber', 'subscriber_id', 'id'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
/**
|
||||
* Funnel Model - DB Model for Automation Funnels
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class Funnel extends Model
|
||||
{
|
||||
private static $type = 'funnels';
|
||||
|
||||
protected $table = 'fc_funnels';
|
||||
|
||||
protected $fillable = [
|
||||
'type',
|
||||
'title',
|
||||
'trigger_name',
|
||||
'status',
|
||||
'conditions',
|
||||
'settings',
|
||||
'created_by',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
public static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($model) {
|
||||
$model->type = self::$type;
|
||||
});
|
||||
|
||||
static::addGlobalScope('type', function ($builder) {
|
||||
$builder->where('fc_funnels.type', '=', self::$type);
|
||||
});
|
||||
}
|
||||
|
||||
public function scopePublished($query)
|
||||
{
|
||||
return $query->where('status', 'published');
|
||||
}
|
||||
|
||||
public function actions()
|
||||
{
|
||||
return $this->hasMany(
|
||||
__NAMESPACE__ . '\FunnelSequence', 'funnel_id', 'id'
|
||||
);
|
||||
}
|
||||
|
||||
public function subscribers()
|
||||
{
|
||||
return $this->hasMany(
|
||||
__NAMESPACE__ . '\FunnelSubscriber', 'funnel_id', 'id'
|
||||
);
|
||||
}
|
||||
|
||||
public function setSettingsAttribute($settings)
|
||||
{
|
||||
$this->attributes['settings'] = \maybe_serialize($settings);
|
||||
}
|
||||
|
||||
public function getSettingsAttribute($settings)
|
||||
{
|
||||
return \maybe_unserialize($settings);
|
||||
}
|
||||
|
||||
public function setConditionsAttribute($conditions)
|
||||
{
|
||||
$this->attributes['conditions'] = \maybe_serialize($conditions);
|
||||
}
|
||||
|
||||
public function getConditionsAttribute($conditions)
|
||||
{
|
||||
return \maybe_unserialize($conditions);
|
||||
}
|
||||
|
||||
public function getSubscribersCount()
|
||||
{
|
||||
return $this->subscribers()->count();
|
||||
}
|
||||
|
||||
public function updateMeta($key, $value)
|
||||
{
|
||||
fluentcrm_update_meta($this->id, __CLASS__, $key, $value);
|
||||
}
|
||||
|
||||
public function getMeta($key, $default = '')
|
||||
{
|
||||
$meta = fluentcrm_get_meta($this->id, __CLASS__, $key);
|
||||
if($meta) {
|
||||
return $meta->value;
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
|
||||
public function deleteMeta($key)
|
||||
{
|
||||
fluentcrm_delete_meta($this->id, __CLASS__, $key);
|
||||
}
|
||||
|
||||
public function labelsTerm()
|
||||
{
|
||||
return $this->belongsToMany(Label::class, 'fc_term_relations', 'object_id', 'term_id')
|
||||
->wherePivot('object_type', __CLASS__);
|
||||
}
|
||||
|
||||
public function labels()
|
||||
{
|
||||
$labelIds = TermRelation::where('object_id', $this->id)
|
||||
->where('object_type', __CLASS__)
|
||||
->pluck('term_id')
|
||||
->toArray();
|
||||
return Label::whereIn('id', $labelIds)->get();
|
||||
}
|
||||
|
||||
public function getFormattedLabels()
|
||||
{
|
||||
$labels = $this->labels();
|
||||
return $labels->map(function ($label) {
|
||||
return [
|
||||
'id' => $label->id,
|
||||
'slug' => $label->slug,
|
||||
'title' => $label->title,
|
||||
'color' => $label->settings['color'] ?? ''
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public function attachLabels($labelIds)
|
||||
{
|
||||
if (!is_array($labelIds)) {
|
||||
$labelIds = [$labelIds];
|
||||
}
|
||||
|
||||
$existingLabelIds = TermRelation::where('object_id', $this->id)
|
||||
->where('object_type', __CLASS__)
|
||||
->pluck('term_id')
|
||||
->toArray();
|
||||
|
||||
$newLabelIds = array_diff($labelIds, $existingLabelIds);
|
||||
|
||||
if (!empty($newLabelIds)) {
|
||||
foreach ($newLabelIds as $labelId) {
|
||||
TermRelation::create([
|
||||
'object_id' => $this->id,
|
||||
'object_type' => __CLASS__,
|
||||
'term_id' => $labelId
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace existing funnel labels with the provided label IDs.
|
||||
*
|
||||
* @param array|int $labelIds
|
||||
* @return $this
|
||||
*/
|
||||
public function syncLabels($labelIds)
|
||||
{
|
||||
if (!is_array($labelIds)) {
|
||||
$labelIds = [$labelIds];
|
||||
}
|
||||
|
||||
$existingLabelIds = TermRelation::where('object_id', $this->id)
|
||||
->where('object_type', __CLASS__)
|
||||
->pluck('term_id')
|
||||
->toArray();
|
||||
|
||||
$labelIds = array_unique(array_filter(array_map('intval', $labelIds)));
|
||||
$labelsToDetach = array_diff($existingLabelIds, $labelIds);
|
||||
$labelsToAttach = array_diff($labelIds, $existingLabelIds);
|
||||
|
||||
if (!empty($labelsToDetach)) {
|
||||
$this->detachLabels($labelsToDetach);
|
||||
}
|
||||
|
||||
if (!empty($labelsToAttach)) {
|
||||
$this->attachLabels($labelsToAttach);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function detachLabels($labelIds)
|
||||
{
|
||||
if (!is_array($labelIds)) {
|
||||
$labelIds = [$labelIds];
|
||||
}
|
||||
|
||||
TermRelation::where('object_id', $this->id)
|
||||
->where('object_type', __CLASS__)
|
||||
->whereIn('term_id', $labelIds)
|
||||
->delete();
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
use FluentCrm\App\Services\BlockParser;
|
||||
use FluentCrm\App\Services\Helper;
|
||||
use FluentCrm\Framework\Support\Arr;
|
||||
|
||||
/**
|
||||
* FunnelCampaign Model - DB Model for Automation Campaigns
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class FunnelCampaign extends Campaign
|
||||
{
|
||||
protected static $type = 'funnel_email_campaign';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
public static function getMock()
|
||||
{
|
||||
$defaultTemplate = Helper::getDefaultEmailTemplate();
|
||||
return [
|
||||
'id' => '',
|
||||
'parent_id' => '',
|
||||
'title' => __('Funnel Campaign Holder', 'fluent-crm'),
|
||||
'status' => 'published',
|
||||
'template_id' => '',
|
||||
'email_subject' => '',
|
||||
'email_pre_header' => '',
|
||||
'email_body' => '',
|
||||
'utm_status' => 0,
|
||||
'utm_source' => '',
|
||||
'utm_medium' => '',
|
||||
'utm_campaign' => '',
|
||||
'utm_term' => '',
|
||||
'utm_content' => '',
|
||||
'design_template' => $defaultTemplate,
|
||||
'settings' => (object)[
|
||||
'template_config' => Helper::getTemplateConfig($defaultTemplate),
|
||||
'mailer_settings' => [
|
||||
'from_name' => '',
|
||||
'from_email' => '',
|
||||
'reply_to_name' => '',
|
||||
'reply_to_email' => '',
|
||||
'is_custom' => 'no'
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function sendToCustomAddresses($addresses = [], $args = [], $refSubscriber = false)
|
||||
{
|
||||
if (!$addresses) {
|
||||
return;
|
||||
}
|
||||
$time = current_time('mysql');
|
||||
foreach ($addresses as $address) {
|
||||
if (!is_email($address)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if the email has any subscriber
|
||||
$subscriber = Subscriber::where('email', $address)->first();
|
||||
if ($subscriber && $subscriber->status != 'subscribed') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We have to handle manually
|
||||
$emailBody = (new BlockParser($refSubscriber))->parse($this->email_body);
|
||||
|
||||
$emailSubject = $this->email_subject;
|
||||
|
||||
if ($refSubscriber) {
|
||||
/**
|
||||
* Filter the campaign email body content.
|
||||
*
|
||||
* This filter allows you to modify the email body content before it is sent to the subscriber.
|
||||
*
|
||||
* @param string $emailBody The email body content.
|
||||
* @param object $refSubscriber The subscriber object reference.
|
||||
* @since 2.7.0
|
||||
*
|
||||
*/
|
||||
$emailBody = apply_filters('fluent_crm/parse_campaign_email_text', $emailBody, $refSubscriber);
|
||||
/**
|
||||
* Filter the email subject text for a campaign.
|
||||
*
|
||||
* This filter allows you to modify the email subject text before it is sent to the subscriber.
|
||||
*
|
||||
* @param string $emailSubject The original email subject text.
|
||||
* @param object $refSubscriber The subscriber object reference.
|
||||
*
|
||||
* @return string The filtered email subject text.
|
||||
* @since 2.7.0
|
||||
*
|
||||
*/
|
||||
$emailSubject = apply_filters('fluent_crm/parse_campaign_email_text', $emailSubject, $refSubscriber);
|
||||
}
|
||||
|
||||
$email = [
|
||||
'campaign_id' => $this->id,
|
||||
'email_address' => $address,
|
||||
'email_subject' => $emailSubject,
|
||||
'email_body' => $emailBody,
|
||||
'created_at' => $time,
|
||||
'updated_at' => $time,
|
||||
'is_parsed' => 1,
|
||||
'note' => __('Email Sent From Funnel', 'fluent-crm')
|
||||
];
|
||||
|
||||
if ($subscriber) {
|
||||
$email['subscriber_id'] = $subscriber->id;
|
||||
}
|
||||
|
||||
if ($args) {
|
||||
$email = wp_parse_args($email, $args);
|
||||
}
|
||||
|
||||
$insertId = CampaignEmail::insert($email);
|
||||
$emailHash = Helper::generateEmailHash($insertId);
|
||||
|
||||
CampaignEmail::where('id', $insertId)
|
||||
->update([
|
||||
'email_hash' => $emailHash
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add one or more subscribers to the campaign
|
||||
* @param array $subscriberIds
|
||||
* @param array $emailArgs extra campaign_email args
|
||||
* @param bool $isModel if the $subscriberIds is collection or not
|
||||
* @return array
|
||||
*/
|
||||
public function subscribe($subscriberIds, $emailArgs = [], $isModel = false)
|
||||
{
|
||||
$updateIds = [];
|
||||
|
||||
$mailHeaders = Helper::getMailHeadersFromSettings(Arr::get($this->settings, 'mailer_settings', []));
|
||||
|
||||
if ($isModel) {
|
||||
$subscribers = $subscriberIds;
|
||||
} else {
|
||||
$subscribers = Subscriber::whereIn('id', $subscriberIds)->get();
|
||||
}
|
||||
|
||||
$sendableStatuses = fluentCrmEmailSendableStatuses();
|
||||
|
||||
foreach ($subscribers as $subscriber) {
|
||||
if (!in_array($subscriber->status, $sendableStatuses, true)) {
|
||||
continue; // We don't want to send emails to non-subscribed members
|
||||
}
|
||||
|
||||
$time = fluentCrmTimestamp();
|
||||
$email = [
|
||||
'campaign_id' => $this->id,
|
||||
'status' => $this->status,
|
||||
'subscriber_id' => $subscriber->id,
|
||||
'email_address' => $subscriber->email,
|
||||
'email_headers' => $mailHeaders,
|
||||
'created_at' => $time,
|
||||
'updated_at' => $time,
|
||||
'email_body' => '',
|
||||
];
|
||||
$subjectItem = $this->guessEmailSubject();
|
||||
$emailSubject = $this->email_subject;
|
||||
|
||||
// Let's create the email body here
|
||||
$rawTemplates = [
|
||||
'raw_html',
|
||||
'visual_builder',
|
||||
'raw_classic'
|
||||
];
|
||||
$emailBody = $this->email_body;
|
||||
if (!in_array($this->design_template, $rawTemplates)) {
|
||||
$emailBody = (new BlockParser($subscriber))->parse($emailBody);
|
||||
}
|
||||
$emailBody = str_replace(['https://fonts.googleapis.com/css2', 'https://fonts.googleapis.com/css'], 'https://fonts.bunny.net/css', $emailBody);
|
||||
/**
|
||||
* Filter the email body content for a campaign.
|
||||
*
|
||||
* This filter allows you to modify the email body content before it is sent to the subscriber.
|
||||
*
|
||||
* @param string $emailBody The original email body content.
|
||||
* @param object $subscriber The subscriber object containing subscriber details.
|
||||
*
|
||||
* @return string The filtered email body content.
|
||||
* @since 2.8.44
|
||||
*
|
||||
*/
|
||||
$emailBody = apply_filters('fluent_crm/parse_campaign_email_text', $emailBody, $subscriber);
|
||||
// email body creation done
|
||||
|
||||
if ($subjectItem && !empty($subjectItem->value)) {
|
||||
$emailSubject = $subjectItem->value;
|
||||
$email['email_subject_id'] = $subjectItem->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the campaign email subject text.
|
||||
*
|
||||
* This filter allows you to modify the email subject text for a campaign.
|
||||
*
|
||||
* @param string $emailSubject The original email subject text.
|
||||
* @param object $subscriber The subscriber object.
|
||||
*
|
||||
* @return string The filtered email subject text.
|
||||
* @since 2.8.40
|
||||
*
|
||||
*/
|
||||
$email['email_subject'] = apply_filters('fluent_crm/parse_campaign_email_text', $emailSubject, $subscriber);
|
||||
|
||||
if ($emailArgs) {
|
||||
$email = wp_parse_args($emailArgs, $email);
|
||||
}
|
||||
|
||||
$inserted = CampaignEmail::create($email);
|
||||
|
||||
$subscriber->campaign_id = $this->id;
|
||||
$subscriber->email_id = $inserted->id;
|
||||
|
||||
$emailHash = Helper::generateEmailHash($inserted->id);
|
||||
|
||||
/**
|
||||
* Filter the email body content of a campaign email.
|
||||
*
|
||||
* This filter allows you to modify the email body content before it is sent to the subscriber.
|
||||
*
|
||||
* @param string $emailBody The email body content.
|
||||
* @param object $subscriber The subscriber object containing subscriber details.
|
||||
*
|
||||
* @return string The filtered email body content.
|
||||
* @since 2.8.44
|
||||
*
|
||||
*/
|
||||
$emailBody = apply_filters('fluent_crm/parse_campaign_email_text', $emailBody, $subscriber);
|
||||
|
||||
|
||||
$trackingType = fluentcrmTrackClicking();
|
||||
|
||||
if ($trackingType) {
|
||||
$campaignUrls = Helper::urlReplaces($emailBody);
|
||||
if ($campaignUrls) {
|
||||
if ($trackingType === 'anonymous') {
|
||||
$emailBody = Helper::attachAnonymousUrls($emailBody, $campaignUrls, $inserted->id, $emailHash);
|
||||
} else {
|
||||
$emailBody = Helper::attachUrls($emailBody, $campaignUrls, $inserted->id, $emailHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
CampaignEmail::where('id', $inserted->id)
|
||||
->update([
|
||||
'email_hash' => $emailHash,
|
||||
'email_body' => $emailBody,
|
||||
'is_parsed' => 1
|
||||
]);
|
||||
|
||||
$updateIds[] = $inserted->id;
|
||||
}
|
||||
|
||||
$emailCount = $this->getEmailCount();
|
||||
if ($emailCount != $this->recipients_count) {
|
||||
$this->recipients_count = $emailCount;
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $updateIds;
|
||||
}
|
||||
|
||||
public function processAndSubscribe($subscriber, $refData = [], $args = [])
|
||||
{
|
||||
foreach ($refData as $refKey => $data) {
|
||||
$subscriber->{$refKey} = $data;
|
||||
}
|
||||
|
||||
/*
|
||||
* Note: We are not using the parse_campaign_email_text filter here
|
||||
* Have a plan to remove this below commented code
|
||||
*/
|
||||
// We have to handle manually
|
||||
// $emailBody = (new BlockParser($this->subscriber))->parse($this->email_body);
|
||||
// $args['email_body'] = apply_filters('fluent_crm/parse_campaign_email_text', $emailBody, $subscriber);
|
||||
// $args['email_subject'] = apply_filters('fluent_crm/parse_campaign_email_text', $this->email_subject, $subscriber);
|
||||
// $args['is_parsed'] = 1;
|
||||
|
||||
return $this->subscribe([$subscriber], $args, true);
|
||||
}
|
||||
|
||||
|
||||
public function getOpenTrackingStatus($globalFallback = true)
|
||||
{
|
||||
return fluentcrmTrackEmailOpen();
|
||||
}
|
||||
|
||||
public function getClickTrackingStatus($globalFallback = true)
|
||||
{
|
||||
return fluentcrmTrackClicking();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
/**
|
||||
* FunnelMetric Model - DB Model for Automation Analytics
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
class FunnelMetric extends Model
|
||||
{
|
||||
protected $table = 'fc_funnel_metrics';
|
||||
|
||||
protected $fillable = [
|
||||
'funnel_id',
|
||||
'sequence_id',
|
||||
'subscriber_id',
|
||||
'benchmark_value',
|
||||
'benchmark_currency',
|
||||
'status',
|
||||
'notes'
|
||||
];
|
||||
|
||||
public function scopeStatus($query, $status = 'completed')
|
||||
{
|
||||
return $query->where('status', $status);
|
||||
}
|
||||
|
||||
public function funnel()
|
||||
{
|
||||
return $this->belongsTo(
|
||||
__NAMESPACE__ . '\Funnel', 'funnel_id', 'id'
|
||||
);
|
||||
}
|
||||
|
||||
public function sequence()
|
||||
{
|
||||
return $this->belongsTo(
|
||||
__NAMESPACE__ . '\FunnelSequence', 'sequence_id', 'id'
|
||||
);
|
||||
}
|
||||
|
||||
public function subscriber()
|
||||
{
|
||||
return $this->belongsTo(
|
||||
__NAMESPACE__ . '\Subscriber', 'subscriber_id', 'id'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
/**
|
||||
* FunnelSequence Model - DB Model for Automation Sequences
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class FunnelSequence extends Model
|
||||
{
|
||||
protected $table = 'fc_funnel_sequences';
|
||||
|
||||
protected $fillable = [
|
||||
'funnel_id',
|
||||
'action_name',
|
||||
'parent_id',
|
||||
'condition_type',
|
||||
'title',
|
||||
'description',
|
||||
'status',
|
||||
'conditions',
|
||||
'settings',
|
||||
'delay',
|
||||
'c_delay',
|
||||
'sequence',
|
||||
'created_by',
|
||||
'type',
|
||||
'note',
|
||||
];
|
||||
|
||||
public static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::updating(function ($model) {
|
||||
if (isset($model->settings) && is_array($model->settings)) {
|
||||
$model->settings = \maybe_serialize($model->settings);
|
||||
}
|
||||
if (isset($model->conditions) && is_array($model->conditions)) {
|
||||
$model->conditions = \maybe_serialize($model->conditions);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function funnel()
|
||||
{
|
||||
return $this->belongsTo(
|
||||
__NAMESPACE__ . '\Funnel', 'funnel_id', 'id'
|
||||
);
|
||||
}
|
||||
|
||||
public function parent()
|
||||
{
|
||||
return $this->belongsTo(
|
||||
__NAMESPACE__ . '\FunnelSequence', 'parent_id', 'id'
|
||||
);
|
||||
}
|
||||
|
||||
public function children()
|
||||
{
|
||||
return $this->hasMany(
|
||||
__NAMESPACE__ . '\FunnelSequence', 'parent_id', 'id'
|
||||
);
|
||||
}
|
||||
|
||||
public function setSettingsAttribute($settings)
|
||||
{
|
||||
if (is_array($settings)) {
|
||||
$this->attributes['settings'] = \maybe_serialize($settings);
|
||||
} else {
|
||||
$this->attributes['settings'] = $settings;
|
||||
}
|
||||
}
|
||||
|
||||
public function getSettingsAttribute($settings)
|
||||
{
|
||||
return \maybe_unserialize($settings);
|
||||
}
|
||||
|
||||
public function setConditionsAttribute($conditions)
|
||||
{
|
||||
if (is_array($conditions)) {
|
||||
$this->attributes['conditions'] = \maybe_serialize($conditions);
|
||||
} else {
|
||||
$this->attributes['conditions'] = $conditions;
|
||||
}
|
||||
}
|
||||
|
||||
public function getConditionsAttribute($conditions)
|
||||
{
|
||||
return \maybe_unserialize($conditions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
/**
|
||||
* FunnelSubscriber Model - DB Model for Automation Subscribers
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
class FunnelSubscriber extends Model
|
||||
{
|
||||
protected $table = 'fc_funnel_subscribers';
|
||||
|
||||
protected $fillable = [
|
||||
'funnel_id',
|
||||
'subscriber_id',
|
||||
'status',
|
||||
'type',
|
||||
'next_sequence',
|
||||
'next_sequence_id',
|
||||
'last_sequence_id',
|
||||
'last_sequence_status',
|
||||
'last_executed_time',
|
||||
'next_execution_time',
|
||||
'starting_sequence_id',
|
||||
'source_trigger_name',
|
||||
'source_ref_id',
|
||||
'notes'
|
||||
];
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('status', 'active');
|
||||
}
|
||||
|
||||
public function funnel()
|
||||
{
|
||||
return $this->belongsTo(
|
||||
__NAMESPACE__ . '\Funnel', 'funnel_id', 'id'
|
||||
);
|
||||
}
|
||||
|
||||
public function next_sequence_item()
|
||||
{
|
||||
return $this->belongsTo(
|
||||
__NAMESPACE__ . '\FunnelSequence', 'next_sequence_id', 'id'
|
||||
);
|
||||
}
|
||||
|
||||
public function last_sequence()
|
||||
{
|
||||
return $this->belongsTo(
|
||||
__NAMESPACE__ . '\FunnelSequence', 'last_sequence_id', 'id'
|
||||
);
|
||||
}
|
||||
|
||||
public function metrics()
|
||||
{
|
||||
return $this->hasMany(
|
||||
__NAMESPACE__ . '\FunnelMetric', 'subscriber_id', 'subscriber_id'
|
||||
);
|
||||
}
|
||||
|
||||
public function subscriber()
|
||||
{
|
||||
return $this->belongsTo(
|
||||
__NAMESPACE__ . '\Subscriber', 'subscriber_id', 'id'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
use FluentCrm\Framework\Database\Orm\Builder;
|
||||
|
||||
class Label extends Model
|
||||
{
|
||||
protected $table = 'fc_terms';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
/*
|
||||
* taxonomy_name: global_label
|
||||
* taxonomy_name global_label is the default taxonomy name/type for the Label
|
||||
* so it is not required to pass the taxonomy_name while creating a label
|
||||
*/
|
||||
protected $fillable = ['parent_id', 'slug', 'title', 'description', 'position', 'settings', 'created_at', 'updated_at'];
|
||||
|
||||
protected $hidden = ['taxonomy_name'];
|
||||
|
||||
public static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($model) {
|
||||
$model->taxonomy_name = $model->taxonomy_name ?: 'global_label'; // default type is label
|
||||
});
|
||||
|
||||
static::addGlobalScope('taxonomy_name', function (Builder $builder) {
|
||||
$builder->where('taxonomy_name', '=', 'global_label');
|
||||
});
|
||||
}
|
||||
|
||||
public function getSettingsAttribute($value)
|
||||
{
|
||||
return \maybe_unserialize($value);
|
||||
}
|
||||
|
||||
public function setSettingsAttribute($value)
|
||||
{
|
||||
$this->attributes['settings'] = maybe_serialize($value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
/**
|
||||
* Lists Model - DB Model for Contact Lists
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
class Lists extends Model
|
||||
{
|
||||
protected $table = 'fc_lists';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
/**
|
||||
* $searchable Columns in table to search
|
||||
* @var array
|
||||
*/
|
||||
protected $searchable = [
|
||||
'title',
|
||||
'slug',
|
||||
'description'
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Local scope to filter subscribers by search/query string
|
||||
* @param ModelQueryBuilder $query
|
||||
* @param string $search
|
||||
* @return ModelQueryBuilder
|
||||
*/
|
||||
public function scopeSearchBy($query, $search)
|
||||
{
|
||||
if ($search) {
|
||||
$fields = $this->searchable;
|
||||
$query->where(function ($query) use ($fields, $search) {
|
||||
$query->where(array_shift($fields), 'LIKE', "%$search%");
|
||||
foreach ($fields as $field) {
|
||||
$query->orWhere($field, 'LIKE', "$search%");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Many2Many: List belongs to many Subscriber
|
||||
*
|
||||
* @return \FluentCrm\App\Models\Base\Collection
|
||||
*/
|
||||
public function subscribers()
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
__NAMESPACE__.'\Subscriber', 'fc_subscriber_pivot', 'object_id', 'subscriber_id'
|
||||
)->where('object_type', __CLASS__);
|
||||
}
|
||||
|
||||
public function totalCount()
|
||||
{
|
||||
return fluentCrmDb()->table('fc_subscriber_pivot')
|
||||
->where('object_type', 'FluentCrm\App\Models\Lists')
|
||||
->where('object_id', $this->id)
|
||||
->count();
|
||||
}
|
||||
|
||||
public function countByStatus($status = 'subscribed')
|
||||
{
|
||||
return fluentCrmDb()->table('fc_subscriber_pivot')
|
||||
->where('fc_subscriber_pivot.object_type', 'FluentCrm\App\Models\Lists')
|
||||
->where('fc_subscriber_pivot.object_id', $this->id)
|
||||
->join('fc_subscribers', 'fc_subscribers.id', '=', 'fc_subscriber_pivot.subscriber_id')
|
||||
->where('fc_subscribers.status', $status)
|
||||
->count();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
/**
|
||||
* Meta Model - DB Model for Meta table
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
class Meta extends Model
|
||||
{
|
||||
protected $table = 'fc_meta';
|
||||
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $fillable = [
|
||||
'object_type',
|
||||
'object_id',
|
||||
'key',
|
||||
'value',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
public function setValueAttribute($value)
|
||||
{
|
||||
$this->attributes['value'] = maybe_serialize($value);
|
||||
}
|
||||
|
||||
public function getValueAttribute($value)
|
||||
{
|
||||
return maybe_unserialize($value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
use FluentCrm\Framework\Database\Orm\Model as BaseModel;
|
||||
use FluentCrm\Framework\Support\Str;
|
||||
|
||||
class Model extends BaseModel
|
||||
{
|
||||
public function __construct($attributes = [])
|
||||
{
|
||||
parent::__construct($attributes);
|
||||
}
|
||||
|
||||
public function scopeLatest($query, $field = 'created_at')
|
||||
{
|
||||
return $query->orderBy($field, 'desc');
|
||||
}
|
||||
|
||||
public function scopeNewest($query, $field = 'created_at')
|
||||
{
|
||||
return $query->orderBy($field, 'asc');
|
||||
}
|
||||
|
||||
public function getPerPage()
|
||||
{
|
||||
return (isset($_REQUEST['per_page'])) ? intval($_REQUEST['per_page']) : 15;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a fresh timestamp for the model.
|
||||
*
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function freshTimestamp()
|
||||
{
|
||||
return new \FluentCrm\Framework\Support\DateTime(current_time('mysql'));
|
||||
}
|
||||
|
||||
protected function serializeDate(\DateTimeInterface $date)
|
||||
{
|
||||
return $date->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
public function getTimezone()
|
||||
{
|
||||
return wp_timezone();
|
||||
}
|
||||
|
||||
protected function asDateTime($value)
|
||||
{
|
||||
if (is_string($value) && Str::contains($value, 'T')) {
|
||||
return new \FluentCrm\Framework\Support\DateTime($value);
|
||||
}
|
||||
|
||||
return parent::asDateTime($value);
|
||||
}
|
||||
|
||||
protected function originalIsNumericallyEquivalent($key)
|
||||
{
|
||||
$current = $this->attributes[$key];
|
||||
|
||||
$original = $this->original[$key];
|
||||
|
||||
return is_numeric($current) && is_numeric($original) && strcmp((string) $current, (string) $original) === 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
/**
|
||||
* Subject Model - DB Model for Email Subjects
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
class Subject extends Model
|
||||
{
|
||||
protected $table = 'fc_meta';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
public static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($model) {
|
||||
$model->object_type = __class__;
|
||||
});
|
||||
static::saving(function ($model) {
|
||||
$model->object_type = __class__;
|
||||
});
|
||||
|
||||
static::addGlobalScope('object_type', function ($builder) {
|
||||
$builder->where('object_type', '=', __class__);
|
||||
});
|
||||
}
|
||||
|
||||
public function campaign()
|
||||
{
|
||||
return $this->belongsTo(__NAMESPACE__.'\Campaign', 'object_id', 'id')
|
||||
->withoutGlobalScope('type');
|
||||
}
|
||||
|
||||
public function emails()
|
||||
{
|
||||
return $this->hasMany(__NAMESPACE__.'\CampaignEmail', 'email_subject_id', 'id');
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
/**
|
||||
* SubscriberMeta Model - DB Model for Contact meta data
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
class SubscriberMeta extends Model
|
||||
{
|
||||
protected $table = 'fc_subscriber_meta';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
/**
|
||||
* One2One: SubscriberNote belongs to one Subscriber
|
||||
* @return \FluentCrm\Framework\Database\Orm\Relations\BelongsTo
|
||||
*/
|
||||
public function subscriber()
|
||||
{
|
||||
return $this->belongsTo(
|
||||
__NAMESPACE__.'\Subscriber', 'subscriber_id', 'id'
|
||||
);
|
||||
}
|
||||
|
||||
public function scopeFilterByKey($query, $key)
|
||||
{
|
||||
if ($key) {
|
||||
$query->where('key', $key);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function setValueAttribute($value)
|
||||
{
|
||||
$this->attributes['value'] = maybe_serialize($value);
|
||||
}
|
||||
|
||||
public function getValueAttribute($value)
|
||||
{
|
||||
return maybe_unserialize($value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
/**
|
||||
* SubscriberNote Model - DB Model for Contact's notes
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class SubscriberNote extends Model
|
||||
{
|
||||
protected $table = 'fc_subscriber_notes';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $fillable = [
|
||||
'subscriber_id',
|
||||
'parent_id',
|
||||
'created_by',
|
||||
'type',
|
||||
'title',
|
||||
'description',
|
||||
'created_at'
|
||||
];
|
||||
|
||||
public static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($model) {
|
||||
if (empty($model->created_at)) {
|
||||
$model->created_at = fluentCrmTimestamp();
|
||||
}
|
||||
|
||||
$model->updated_at = fluentCrmTimestamp();
|
||||
$model->created_by = $model->created_by ?: get_current_user_id();
|
||||
});
|
||||
|
||||
static::updated(function ($model) {
|
||||
$model->updated_at = fluentCrmTimestamp();
|
||||
});
|
||||
|
||||
static::addGlobalScope('status', function ($builder) {
|
||||
$builder->whereNotIn('status', ['_company_note_', '_system_log_']);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* One2One: SubscriberNote belongs to one Subscriber
|
||||
* @return \FluentCrm\Framework\Database\Orm\Relations\BelongsTo
|
||||
*/
|
||||
public function subscriber()
|
||||
{
|
||||
return $this->belongsTo(
|
||||
__NAMESPACE__ . '\Subscriber', 'subscriber_id', 'id'
|
||||
);
|
||||
}
|
||||
|
||||
public function markAs($status)
|
||||
{
|
||||
$this->status = $status;
|
||||
$this->save();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function createdBy()
|
||||
{
|
||||
if (!$this->created_by) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$user = User::find($this->created_by);
|
||||
|
||||
if (!$user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
'ID' => $user->ID,
|
||||
'display_name' => $user->display_name,
|
||||
'photo' => $user->photo
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
|
||||
/**
|
||||
* SubscriberPivot Model - DB Model for Contact's relationships
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
class SubscriberPivot extends Model
|
||||
{
|
||||
protected $table = 'fc_subscriber_pivot';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
public function scopeFilter($query, $constraints)
|
||||
{
|
||||
foreach ($constraints as $filed => $value) {
|
||||
$query->where($filed, $value);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an entry to the subscriber pivot table.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @return int
|
||||
*/
|
||||
public static function store($attributes)
|
||||
{
|
||||
$attributes += [
|
||||
'created_at' => $now = current_time('mysql'),
|
||||
'updated_at' => $now
|
||||
];
|
||||
|
||||
return static::insert($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach tags/lists to the subscriber.
|
||||
*
|
||||
* @param array $items
|
||||
* @param int $subscriber
|
||||
* @param string $type
|
||||
*/
|
||||
public static function attach($items, $subscriber, $type)
|
||||
{
|
||||
$objectIds = [];
|
||||
|
||||
foreach ($items as $objectId) {
|
||||
$objectIds = array_merge($objectIds, [$objectId]);
|
||||
static::firstOrCreate([
|
||||
'subscriber_id' => $subscriber,
|
||||
'object_id' => $objectId,
|
||||
'object_type' => $type
|
||||
]);
|
||||
}
|
||||
|
||||
if ($objectIds) {
|
||||
$function = static::getFunctionName($type, __FUNCTION__);
|
||||
$function($objectIds, Subscriber::find($subscriber));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach tags/lists from the subscriber.
|
||||
*
|
||||
* @param array $items
|
||||
* @param int $subscriber
|
||||
* @param string $type
|
||||
*/
|
||||
public static function detach($items, $subscriber, $type)
|
||||
{
|
||||
if ($items) {
|
||||
static::where('subscriber_id', $subscriber)
|
||||
->where('object_type', $type)
|
||||
->whereIn('object_id', $items)
|
||||
->delete();
|
||||
|
||||
$function = static::getFunctionName($type, __FUNCTION__);
|
||||
$function($items, Subscriber::find($subscriber));
|
||||
}
|
||||
}
|
||||
|
||||
private static function getFunctionName($type, $prefix)
|
||||
{
|
||||
$parts = explode('\\', $type);
|
||||
$typeOfObject = end($parts);
|
||||
$function = $typeOfObject == 'Tag' ? 'tags' : 'lists';
|
||||
|
||||
if ($prefix == 'attach') {
|
||||
return "fluentcrm_contact_added_to_$function";
|
||||
} else if ($prefix == 'detach') {
|
||||
return "fluentcrm_contact_removed_from_$function";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
/**
|
||||
* System Log Model - DB Model for System Logs & Activities
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class SystemLog extends Model
|
||||
{
|
||||
protected $table = 'fc_subscriber_notes';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $fillable = [
|
||||
'subscriber_id',
|
||||
'parent_id',
|
||||
'created_by',
|
||||
'type',
|
||||
'title',
|
||||
'description',
|
||||
'created_at'
|
||||
];
|
||||
|
||||
public static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($model) {
|
||||
if (empty($model->created_at)) {
|
||||
$model->created_at = fluentCrmTimestamp();
|
||||
}
|
||||
|
||||
if (empty($model->subscriber_id)) {
|
||||
$model->subscriber_id = 0;
|
||||
}
|
||||
|
||||
$model->status = '_system_log_';
|
||||
|
||||
$model->updated_at = fluentCrmTimestamp();
|
||||
});
|
||||
|
||||
static::updated(function ($model) {
|
||||
$model->updated_at = fluentCrmTimestamp();
|
||||
});
|
||||
|
||||
static::addGlobalScope('status', function ($builder) {
|
||||
$builder->where('status', '_system_log_');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
/**
|
||||
* Tag Model - DB Model for Contact's Tags
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
class Tag extends Model
|
||||
{
|
||||
protected $table = 'fc_tags';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
/**
|
||||
* $searchable Columns in table to search
|
||||
* @var array
|
||||
*/
|
||||
protected $searchable = [
|
||||
'title',
|
||||
'slug',
|
||||
'description'
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Local scope to filter subscribers by search/query string
|
||||
* @param \FluentCrm\Framework\Database\Query\Builder $query
|
||||
* @param string $search
|
||||
* @return \FluentCrm\Framework\Database\Query\Builder
|
||||
*/
|
||||
public function scopeSearchBy($query, $search)
|
||||
{
|
||||
if ($search) {
|
||||
$fields = $this->searchable;
|
||||
$query->where(function ($query) use ($fields, $search) {
|
||||
$query->where(array_shift($fields), 'LIKE', "%$search%");
|
||||
foreach ($fields as $field) {
|
||||
$query->orWhere($field, 'LIKE', "$search%");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the subscribers that belongs to the tag.
|
||||
*
|
||||
* @return \FluentCrm\Framework\Database\Orm\Relations\BelongsToMany
|
||||
*/
|
||||
public function subscribers()
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
__NAMESPACE__.'\Subscriber', 'fc_subscriber_pivot', 'object_id', 'subscriber_id'
|
||||
)->where('object_type', __CLASS__);
|
||||
}
|
||||
|
||||
public function totalCount()
|
||||
{
|
||||
return fluentCrmDb()->table('fc_subscriber_pivot')
|
||||
->where('object_type', 'FluentCrm\App\Models\Tag')
|
||||
->where('object_id', $this->id)
|
||||
->count();
|
||||
}
|
||||
|
||||
public function countByStatus($status = 'subscribed')
|
||||
{
|
||||
return fluentCrmDb()->table('fc_subscriber_pivot')
|
||||
->where('fc_subscriber_pivot.object_type', 'FluentCrm\App\Models\Tag')
|
||||
->where('fc_subscriber_pivot.object_id', $this->id)
|
||||
->join('fc_subscribers', 'fc_subscribers.id', '=', 'fc_subscriber_pivot.subscriber_id')
|
||||
->where('fc_subscribers.status', $status)
|
||||
->count();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
use FluentCrm\App\Services\Libs\Parser\Parser;
|
||||
|
||||
/**
|
||||
* Template Model - DB Model for Email templates
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
class Template extends Model
|
||||
{
|
||||
const CREATED_AT = 'post_date';
|
||||
|
||||
const UPDATED_AT = 'post_modified';
|
||||
|
||||
protected $table = 'posts';
|
||||
|
||||
protected $primaryKey = 'ID';
|
||||
|
||||
public function scopeEmailTemplates($query, $types = ['publish'])
|
||||
{
|
||||
return $query->where(
|
||||
'post_type', fluentcrmTemplateCPTSlug()
|
||||
)->whereIn('post_status', $types);
|
||||
}
|
||||
|
||||
public function scopeCampaignTemplate($query)
|
||||
{
|
||||
return $query->where(
|
||||
'post_type', fluentcrmCampaignTemplateCPTSlug()
|
||||
)->where('post_status', 'publish');
|
||||
}
|
||||
|
||||
public function campaign()
|
||||
{
|
||||
return $this->hasOne(__NAMESPACE__.'\\'.'Campaign', 'template_id', 'ID');
|
||||
}
|
||||
|
||||
public function render($content = null)
|
||||
{
|
||||
$content = $content ?: $this->post_content;
|
||||
|
||||
return Parser::parse($content, []);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
use FluentCrm\Framework\Database\Orm\Model;
|
||||
|
||||
class TermRelation extends Model
|
||||
{
|
||||
protected $table = 'fc_term_relations';
|
||||
|
||||
protected $fillable = [
|
||||
'term_id',
|
||||
'object_type',
|
||||
'object_id',
|
||||
'settings'
|
||||
];
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $casts = [
|
||||
'settings' => 'array'
|
||||
];
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
/**
|
||||
* UrlStores Model - DB Model for Short Urls
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class UrlStores extends Model
|
||||
{
|
||||
protected $table = 'fc_url_stores';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
public static function getUrlSlug($longUrl)
|
||||
{
|
||||
// Normalize URL before lookup and storage
|
||||
$longUrl = str_replace("\xE2\x80\x8B", '', $longUrl);
|
||||
$longUrl = htmlspecialchars_decode($longUrl);
|
||||
|
||||
static $urls = [];
|
||||
|
||||
$cacheKey = md5($longUrl);
|
||||
|
||||
if (isset($urls[$cacheKey])) {
|
||||
return $urls[$cacheKey];
|
||||
}
|
||||
|
||||
$isExist = self::where('url', $longUrl)->first();
|
||||
|
||||
if ($isExist) {
|
||||
$urls[$cacheKey] = $isExist->short;
|
||||
return $isExist->short;
|
||||
}
|
||||
|
||||
$maxRetries = 3;
|
||||
for ($attempt = 0; $attempt < $maxRetries; $attempt++) {
|
||||
$short = self::generateRandomSlug();
|
||||
try {
|
||||
self::insert([
|
||||
'url' => $longUrl,
|
||||
'short' => $short,
|
||||
'created_at' => current_time('mysql'),
|
||||
'updated_at' => current_time('mysql')
|
||||
]);
|
||||
$urls[$cacheKey] = $short;
|
||||
return $short;
|
||||
} catch (\Exception $e) {
|
||||
if (strpos($e->getMessage(), 'Duplicate entry') !== false) {
|
||||
continue;
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public static function generateRandomSlug($length = 6)
|
||||
{
|
||||
$chars = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
$charsLen = strlen($chars);
|
||||
|
||||
$slug = '';
|
||||
$bytes = random_bytes($length);
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$slug .= $chars[ord($bytes[$i]) % $charsLen];
|
||||
}
|
||||
|
||||
return $slug;
|
||||
}
|
||||
|
||||
public static function getRowByShort($short)
|
||||
{
|
||||
global $wpdb;
|
||||
return $wpdb->get_row($wpdb->prepare("SELECT * FROM " . $wpdb->prefix . "fc_url_stores WHERE BINARY `short` = %s ORDER BY `id` DESC LIMIT 1", $short));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
/**
|
||||
* User Model - DB Model for WordPress Users Table
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class User extends Model
|
||||
{
|
||||
protected $table = 'users';
|
||||
|
||||
protected $primaryKey = 'ID';
|
||||
|
||||
protected $hidden = ['user_pass', 'user_activation_key'];
|
||||
|
||||
protected $appends = [ 'photo'];
|
||||
|
||||
/**
|
||||
* Accessor to get dynamic photo attribute
|
||||
* @return string
|
||||
*/
|
||||
public function getPhotoAttribute()
|
||||
{
|
||||
$contact = Subscriber::where('user_id', $this->ID);
|
||||
|
||||
if(!empty($this->attributes['user_email'])) {
|
||||
$contact->orWhere('email', $this->attributes['user_email']);
|
||||
}
|
||||
|
||||
$contact = $contact->first();
|
||||
|
||||
if($contact) {
|
||||
return $contact->photo;
|
||||
}
|
||||
|
||||
if(empty($this->attributes['user_email'])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return fluentcrmGravatar($this->attributes['user_email'], $this->attributes['display_name']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Models;
|
||||
|
||||
use FluentCrm\App\Services\Helper;
|
||||
use FluentCrm\Framework\Support\Arr;
|
||||
|
||||
/**
|
||||
* Webhook Model - DB Model for Webhooks
|
||||
*
|
||||
* Database Model
|
||||
*
|
||||
* @package FluentCrm\App\Models
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
class Webhook extends Meta
|
||||
{
|
||||
protected $fillable = [
|
||||
'id',
|
||||
'key',
|
||||
'value',
|
||||
'object_type'
|
||||
];
|
||||
|
||||
public static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::addGlobalScope('type', function ($builder) {
|
||||
$builder->where(function($query) {
|
||||
$query->where('object_type', '=', 'webhook')
|
||||
->orWhere('object_type', 'LIKE', 'webhook_%');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public function getFields()
|
||||
{
|
||||
$contactFields = [
|
||||
'fields' => [],
|
||||
'custom_fields' => []
|
||||
];
|
||||
|
||||
foreach (Subscriber::mappables() as $key => $column) {
|
||||
$contactFields['fields'][] = ['key' => $key, 'field' => $column];
|
||||
}
|
||||
|
||||
foreach ((new CustomContactField)->getGlobalFields()['fields'] as $field) {
|
||||
$contactFields['custom_fields'][] = ['key' => $field['slug'], 'field' => $field['label']];
|
||||
}
|
||||
|
||||
return $contactFields;
|
||||
}
|
||||
|
||||
public function getSchema()
|
||||
{
|
||||
$schema = [
|
||||
'name' => '',
|
||||
'lists' => [],
|
||||
'tags' => [],
|
||||
'url' => '',
|
||||
'status' => ''
|
||||
];
|
||||
|
||||
if (Helper::isCompanyEnabled()) {
|
||||
$schema['companies'] = [];
|
||||
}
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
public function store($data)
|
||||
{
|
||||
$key = wp_generate_uuid4();
|
||||
$webhookUrl = site_url("?fluentcrm=1&route=contact&hash={$key}");
|
||||
|
||||
return static::create([
|
||||
'object_type' => 'webhook',
|
||||
'key' => $key,
|
||||
'value' => array_merge($data, [
|
||||
'url' => $webhookUrl
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function saveChanges($data)
|
||||
{
|
||||
$data['tags'] = Arr::get($data, 'tags', []);
|
||||
$data['lists'] = Arr::get($data, 'lists', []);
|
||||
$data['companies'] = Arr::get($data, 'companies', []);
|
||||
|
||||
$this->value = array_merge(
|
||||
$this->value,
|
||||
array_diff_key($data, [
|
||||
'id' => '', 'url' => ''
|
||||
])
|
||||
);
|
||||
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user