<?php
namespace App\Entity\System\Twilio;
use App\Entity\System\AbstractMessagingConfig;
use App\Entity\System\Twilio\TwilioConfigs;
use Cms\CoreBundle\Model\Interfaces\Identifiable\IdentifiableInterface;
use Doctrine\ORM\Mapping as ORM;
use JsonSerializable;
use Ramsey\Uuid\UuidInterface;
/**
* Class TwilioConfig
* @package App\Entity\System\Twilio
*
* @ORM\Entity(
* repositoryClass = "App\Doctrine\Repository\System\Twilio\TwilioConfigRepository",
* )
* @ORM\Table(
* name = "sys__twilio_config",
* )
*/
class TwilioConfig extends AbstractMessagingConfig implements JsonSerializable
{
/** @see https://www.twilio.com/docs/sms/a2p-10dlc/onboarding-isv-api?code-sample=code-fetch-possible-a2p-campaign-use-cases&code-language=PHP&code-sdk-version=6.x */
const SMS_CAMPAIGN_USE_CASE__DEFAULT = 'MIXED';
const SMS_CAMPAIGN_USE_CASE__501C3 = 'CHARITY';
const SMS_CAMPAIGN_DESCRIPTION = 'Urgent messages related to school closures/lockdowns, health/safety issues, event/fundraiser reminders, account maintenance notifications, and OTP.';
const SMS_CAMPAIGN_MESSAGE_FLOW = 'Our customers (K-12 schools/districts and secondary education organizations) provide phone numbers of parents and staff for communication purposes. Pursuant to FCC ruling 16-88 (https://docs.fcc.gov/public/attachments/FCC-16-88A1.pdf), these contacts are auto-enrolled in "emergency" communications (school closures, lockdowns, etc). For non-emergency communication, users must sign into a web portal or utilize our mobile app to further opt-in to such non-emergency SMS communications. Users can opt-out of any kind of communication (emergency or non-emergency) by utilizing the web portal, mobile app, by replying with STOP to the number they receive the communication from, or by contacting a school administrator who performs the opt-out on the user\'s behalf via our administrative dashboard.';
const SMS_CAMPAIGN_SAMPLES = [
'LCSC: School campuses are closed Wed. Aug. 9 due to the city snow emergency. It will become an e-learning day. Teachers do not report. https://cmps.st/12345678',
'GMS: 7th and 8th Grade volleyball is tonight @Jac-Cen-Del at 5:30pm. Thursday @Batesville starts at 6pm. https://cmps.st/12345678',
'Welcome to the 2021-22 school year! Use the following link to set your preferences on how to receive school notifications. https://cmps.st/12345678',
'SchoolNow: You requested to manage contact info. Your login code expires in 15 minutes and is: 123456. Do NOT share this code; we will NEVER call to ask for it.',
];
const SMS_CAMPAIGN_USES_LINKS = true;
const SMS_CAMPAIGN_USES_PHONES = true;
use TwilioConfigs\TwilioConfigSubaccountTrait;
use TwilioConfigs\TwilioConfigApiTrait;
use TwilioConfigs\TwilioConfigInfoTrait;
use TwilioConfigs\TwilioConfigBusinessTrait;
use TwilioConfigs\TwilioConfigPhoneTrait;
use TwilioConfigs\TwilioConfigCallerIdTrait;
use TwilioConfigs\TwilioConfigMessagingTrait;
use TwilioConfigs\TwilioConfigSmsTrait;
use TwilioConfigs\TwilioConfigVoiceTrait;
use TwilioConfigs\TwilioConfigCnamTrait;
/**
*
*/
public function __construct()
{
$this->__constructSubaccount();
$this->__constructApi();
$this->__constructInfo();
$this->__constructBusiness();
$this->__constructPhone();
$this->__constructCallerId();
$this->__constructMessaging();
$this->__constructSms();
$this->__constructVoice();
$this->__constructCnam();
}
/**
* @param array $evaluation
* @return array|string[]
*/
protected function reduceEvaluation(array $evaluation): array
{
$errors = [];
if ($evaluation['status'] !== 'compliant') {
foreach ($evaluation['results'] as $result) {
if ( ! $result['passed']) {
if ($result['failure_reason']) {
$errors[] = sprintf(
'%s [%s]: %s',
$result['requirement_friendly_name'],
$result['error_code'],
$result['failure_reason']
);
}
foreach ($result['fields'] as $field) {
if ( ! $field['passed']) {
$errors[] = sprintf(
'%s | %s [%s]: %s',
$result['requirement_friendly_name'],
$field['friendly_name'],
$field['error_code'],
$field['failure_reason']
);
}
}
}
}
}
return $errors;
}
/**
* @return bool
*/
public function isSmsUsable(): bool
{
// we must be provisioned and have a phone number present in order to be usable
return (
$this->isProvisioned()
&&
$this->getIncomingPhoneNumberSid()
);
}
/**
* @return bool
*/
public function isVoiceUsable(): bool
{
// we must be provisioned and have a phone number present in order to be usable
return (
$this->isProvisioned()
&&
$this->getIncomingPhoneNumberSid()
);
}
/**
* {@inheritDoc}
*/
public function isUsable(): bool
{
return (
$this->isSmsUsable()
&&
$this->isVoiceUsable()
);
}
/**
* Mainly used to determine if we are able to properly search for phone numbers for this customer.
*
* @return bool
*/
public function isLocateable(): bool
{
return (
$this->getBusinessInfo()->getPhone()
&&
$this->getAddressInfo()->getCountry()
);
}
/**
* {@inheritDoc}
*/
public function isCredentialed(): bool
{
// all we need is a subaccount setup along with api creds
// we can assume if we have an api key id, that we have the secret and stuff too...
return (
$this->getSubaccountSid()
&&
$this->getApiKeySid()
);
}
/**
* {@inheritDoc}
*/
public function isProvisioned(): bool
{
// all we need is a subaccount setup along with api creds
// we can assume if we have an api key id, that we have the secret and stuff too...
return (
$this->isCredentialed()
&&
$this->getTwimlAppSid()
&&
$this->getMessagingServiceSid()
);
}
/**
* {@inheritDoc}
*/
public function canOrganizationRegister(): bool
{
// check the parent requirements first
$check = parent::canOrganizationRegister();
// only check the other things if we passed the parent test
if ($check) {
// must loop over all the information collectors
// if any one of these is not registerable, it fails the whole check
foreach ($this->getInfos() as $info) {
if ( ! $info->isRegisterable()) {
$check = false;
break;
}
}
}
return $check;
}
/**
* {@inheritDoc}
*/
public function isOrganizationRegistered(): bool
{
return (
$this->getBusinessProfileSid()
&&
$this->getAssignmentBusinessProfileSid()
&&
$this->getAddressSid()
&&
$this->getSupportingDocumentSid()
&&
$this->getAssignmentSupportingDocumentSid()
&&
$this->getBusinessInformationSid()
&&
$this->getAssignmentBusinessInformationSid()
&&
$this->getPrimaryRepresentativeSid()
&&
$this->getAssignmentPrimaryRepresentativeSid()
&&
$this->getSecondaryRepresentativeSid()
&&
$this->getAssignmentSecondaryRepresentativeSid()
&&
$this->getAssignmentPhoneChannelEndpointSids()
&&
$this->checksum() === $this->getPersistedChecksum()
);
}
/**
* {@inheritDoc}
*/
public function isOrganizationEvaluated(): bool
{
return (
$this->isOrganizationRegistered()
&&
$this->getEvaluationStatus() === 'compliant'
);
}
/**
* {@inheritDoc}
*/
public function isOrganizationSubmitted(): bool
{
return (
$this->isOrganizationRegistered()
&&
$this->getStatus()
&&
$this->getStatus() !== 'draft'
);
}
/**
* {@inheritDoc}
*/
public function isOrganizationVerified(): bool
{
return (
$this->isOrganizationRegistered()
&&
in_array($this->getStatus(), ['twilio-approved','twilio-rejected'])
);
}
/**
* {@inheritDoc}
*/
public function isOrganizationValid(): bool
{
return (
$this->isOrganizationVerified()
&&
$this->getStatus() === 'twilio-approved'
);
}
/**
* {@inheritDoc}
*/
public function isOrganizationInvalid(): bool
{
return (
$this->isOrganizationVerified()
&&
$this->getStatus() === 'twilio-rejected'
);
}
/**
* {@inheritDoc}
*/
public function canPrepare(): bool
{
return (
$this->isProvisioned()
&&
$this->isLocateable()
);
}
/**
* {@inheritDoc}
*/
public function isPrepared(): bool
{
return $this->hasPhoneNumbers();
}
/**
* @return bool
*/
public function isSmsServiceRegistered(): bool
{
return (
$this->getSmsTrustProductSid()
&&
$this->getSmsMessagingProfileInformationSid()
&&
$this->getSmsAssignmentMessagingProfileInformationSid()
&&
$this->getSmsAssignmentPrimaryTrustProductSid()
&&
$this->getSmsAssignmentTrustProductSid()
);
}
/**
* @return bool
*/
public function isVoiceServiceRegistered(): bool
{
return (
$this->getVoiceTrustProductSid()
&&
$this->getVoiceAssignmentTrustProductSid()
&&
$this->getVoiceAssignmentPrimaryTrustProductSid()
&&
(count($this->getVoiceAssignmentChannelEndpointSids()) === count($this->getIncomingPhoneNumberSids()))
);
}
/**
* @return bool
*/
public function isCnamServiceRegistered(): bool
{
return (
$this->getCnamTrustProductSid()
&&
$this->getCnamAssignmentTrustProductSid()
&&
$this->getCnamAssignmentPrimaryTrustProductSid()
&&
$this->getCnamInformationSid()
&&
$this->getCnamAssignmentInformationSid()
&&
(count($this->getCnamAssignmentChannelEndpointSids()) === count($this->getIncomingPhoneNumberSids()))
);
}
/**
* {@inheritDoc}
*/
public function isServicesRegistered(): bool
{
return (
$this->isSmsServiceRegistered()
&&
$this->isVoiceServiceRegistered()
&&
$this->isCnamServiceRegistered()
);
}
/**
* @return bool
*/
public function isSmsServiceEvaluated(): bool
{
return ($this->getSmsEvaluationStatus() === 'compliant');
}
/**
* @return bool
*/
public function isVoiceServiceEvaluated(): bool
{
return ($this->getVoiceEvaluationStatus() === 'compliant');
}
/**
* @return bool
*/
public function isCnamServiceEvaluated(): bool
{
return ($this->getCnamEvaluationStatus() === 'compliant');
}
/**
* {@inheritDoc}
*/
public function isServicesEvaluated(): bool
{
return (
$this->isSmsServiceEvaluated()
&&
$this->isVoiceServiceEvaluated()
&&
$this->isCnamServiceEvaluated()
);
}
/**
* @return bool
*/
public function isSmsServiceSubmitted(): bool
{
return in_array($this->getSmsStatus(), ['pending-review','in-review']);
}
/**
* @return bool
*/
public function isVoiceServiceSubmitted(): bool
{
return in_array($this->getVoiceStatus(), ['pending-review','in-review']);
}
/**
* @return bool
*/
public function isCnamServiceSubmitted(): bool
{
return in_array($this->getCnamStatus(), ['pending-review','in-review']);
}
/**
* {@inheritDoc}
*/
public function isServicesSubmitted(): bool
{
return (
$this->isSmsServiceSubmitted()
&&
$this->isVoiceServiceSubmitted()
&&
$this->isCnamServiceSubmitted()
);
}
/**
* @return bool
*/
public function isSmsServiceVerified(): bool
{
return in_array($this->getSmsStatus(), ['twilio-approved','twilio-rejected']);
}
/**
* @return bool
*/
public function isVoiceServiceVerified(): bool
{
return in_array($this->getVoiceStatus(), ['twilio-approved','twilio-rejected']);
}
/**
* @return bool
*/
public function isCnamServiceVerified(): bool
{
return in_array($this->getCnamStatus(), ['twilio-approved','twilio-rejected']);
}
/**
* {@inheritDoc}
*/
public function isServicesVerified(): bool
{
return (
$this->isSmsServiceVerified()
&&
$this->isVoiceServiceVerified()
&&
$this->isCnamServiceVerified()
);
}
/**
* @return bool
*/
public function isSmsServiceValid(): bool
{
return ($this->getSmsStatus() === 'twilio-approved');
}
/**
* @return bool
*/
public function isVoiceServiceValid(): bool
{
return ($this->getVoiceStatus() === 'twilio-approved');
}
/**
* @return bool
*/
public function isCnamServiceValid(): bool
{
return ($this->getCnamStatus() === 'twilio-approved');
}
/**
* {@inheritDoc}
*/
public function isServicesValid(): bool
{
return (
$this->isSmsServiceValid()
&&
$this->isVoiceServiceValid()
&&
$this->isCnamServiceValid()
);
}
/**
* @return bool
*/
public function isSmsServiceInvalid(): bool
{
return ($this->getSmsStatus() === 'twilio-rejected');
}
/**
* @return bool
*/
public function isVoiceServiceInvalid(): bool
{
return ($this->getVoiceStatus() === 'twilio-rejected');
}
/**
* @return bool
*/
public function isCnamServiceInvalid(): bool
{
return ($this->getCnamStatus() === 'twilio-rejected');
}
/**
* {@inheritDoc}
*/
public function isServicesInvalid(): bool
{
return (
$this->isSmsServiceInvalid()
||
$this->isVoiceServiceInvalid()
||
$this->isCnamServiceInvalid()
);
}
/**
* {@inheritDoc}
*/
public function canBrandSubmit(): bool
{
return (
$this->isSmsServiceSubmitted()
||
$this->isSmsServiceValid()
);
}
/**
* {@inheritDoc}
*/
public function isBrandSubmitted(): bool
{
// as soon as you create the brand, it is submitted
// TODO: we can check for PENDING status, but not really needed?
return boolval($this->getSmsBrandRegistrationSid());
}
/**
* {@inheritDoc}
*/
public function isBrandVerified(): bool
{
return in_array($this->getSmsBrandRegistrationStatus(), ['APPROVED','FAILED']);
}
/**
* {@inheritDoc}
*/
public function isBrandValid(): bool
{
return ($this->getSmsBrandRegistrationStatus() === 'APPROVED');
}
/**
* {@inheritDoc}
*/
public function isBrandInvalid(): bool
{
return ($this->getSmsBrandRegistrationStatus() === 'FAILED');
}
/**
* {@inheritDoc}
*/
public function canCampaignsSubmit(): bool
{
return $this->isBrandValid();
}
/**
* {@inheritDoc}
*/
public function isCampaignsSubmitted(): bool
{
// as soon as you create the campaign, it is submitted
return boolval($this->getSmsCampaignSid());
}
/**
* {@inheritDoc}
*/
public function isCampaignsVerified(): bool
{
return in_array($this->getSmsCampaignStatus(), ['VERIFIED','FAILED']);
}
/**
* {@inheritDoc}
*/
public function isCampaignsValid(): bool
{
return ($this->getSmsCampaignStatus() === 'VERIFIED');
}
/**
* {@inheritDoc}
*/
public function isCampaignsInvalid(): bool
{
return ($this->getSmsCampaignStatus() === 'FAILED');
}
/**
* @return bool
*/
public function isSetup(): bool
{
return ($this->getTwimlAppSid() && $this->getMessagingServiceSid());
}
/**
* @return bool
*/
public function isConfigured(): bool
{
return boolval($this->getSmsCampaignSid());
}
/**
* @return bool
*/
public function isSmsReady(): bool
{
return (
$this->isProvisioned()
&&
$this->isOrganizationValid()
&&
$this->isSmsServiceValid()
&&
$this->isBrandValid()
&&
$this->isCampaignsValid()
);
}
/**
* @return bool
*/
public function isVoiceReady(): bool
{
return (
$this->isProvisioned()
&&
$this->isOrganizationValid()
&&
$this->isVoiceServiceValid()
);
}
/**
* @return string
*/
public function checksum(): string
{
return md5(implode('|', [
$this->getBusinessInfo()->checksum(),
$this->getAddressInfo()->checksum(),
$this->getPrimaryContactInfo()->checksum(),
$this->getSecondaryContactInfo()->checksum(),
]));
}
/**
* @return string
*/
public function checksumPayload(): string
{
return implode('|', [
$this->getBusinessInfo()->checksumPayload(),
$this->getAddressInfo()->checksumPayload(),
$this->getPrimaryContactInfo()->checksumPayload(),
$this->getSecondaryContactInfo()->checksumPayload(),
]);
}
/**
* @return array|array[]
*/
public function statuses(): array
{
return [
'business' => [
'status' => $this->getStatus(),
'timestamp' => $this->getStatusTimestamp(),
],
'sms.a2p-10dlc' => [
'status' => $this->getSmsStatus(),
'timestamp' => $this->getSmsStatusTimestamp(),
],
'sms.brand' => [
'status' => $this->getSmsBrandRegistrationStatus(),
'timestamp' => $this->getSmsBrandRegistrationStatusTimestamp(),
],
'sms.campaign' => [
'status' => $this->getSmsCampaignStatus(),
'timestamp' => $this->getSmsCampaignStatusTimestamp(),
],
'voice.shaken-stir' => [
'status' => $this->getVoiceStatus(),
'timestamp' => $this->getVoiceStatusTimestamp(),
],
'voice.cnam' => [
'status' => $this->getCnamStatus(),
'timestamp' => $this->getCnamStatusTimestamp(),
],
];
}
/**
* {@inheritDoc}
*/
public function jsonSerialize(): array
{
static $refl = null;
$formatter = static function ($value, callable $formatter) {
switch (true) {
case is_scalar($value):
return $value;
case is_array($value):
$data = [];
foreach ($value as $k => $v) {
$data[$k] = $formatter($v, $formatter);
}
ksort($data);
return $data;
case $value instanceof UuidInterface:
return $value->__toString();
case $value instanceof IdentifiableInterface:
return $value->getId();
case $value instanceof \DateTimeInterface:
return $value->format(\DateTimeInterface::RFC3339);
case $value instanceof JsonSerializable:
return $value->jsonSerialize();
default:
return null;
}
};
if ( ! $refl) {
$refl = new \ReflectionObject($this);
}
$data = [];
foreach ($refl->getProperties(\ReflectionProperty::IS_PROTECTED) as $property) {
$property->setAccessible(true);
$data[$property->getName()] = $formatter(
$property->getValue($this),
$formatter
);
$property->setAccessible(false);
}
ksort($data);
return $data;
}
}