string, 'content_type' => string, 'id' => string|null] */ public array $media = []; /** * Location latitude (for location messages) */ public ?float $latitude = null; /** * Location longitude (for location messages) */ public ?float $longitude = null; /** * Location address (for location messages) */ public ?string $locationAddress = null; /** * Location name (for location messages) */ public ?string $locationName = null; /** * Interactive message type (button, list, etc.) */ public ?string $interactiveType = null; /** * Interactive list reply ID */ public ?string $listReplyId = null; /** * Interactive list reply title */ public ?string $listReplyTitle = null; /** * Timestamp when message was received */ public ?string $timestamp = null; /** * Raw payload from provider (for debugging/custom processing) */ public array $rawPayload = []; /** * Provider-specific account ID */ public ?string $accountId = null; /** * Business Scoped User ID (BSUID) from Meta/Twilio ExternalUserId * Format: CC.alphanumeric (e.g., "BR.1A2B3C4D5E...") */ public ?string $externalUserId = null; /** * Whether the fromPhone is actually a BSUID (user hid their phone number) */ public bool $isBsuid = false; /** * Create a new NormalizedMessage instance * * @param string $provider */ public function __construct(string $provider) { $this->provider = $provider; $this->timestamp = current_time('mysql'); } /** * Create from array * * @param array $data * @return static */ public static function fromArray(array $data): static { $message = new static($data['provider'] ?? 'unknown'); foreach ($data as $key => $value) { if (property_exists($message, $key)) { $message->$key = $value; } } return $message; } /** * Convert to array * * @return array */ public function toArray(): array { return [ 'message_id' => $this->messageId, 'provider' => $this->provider, 'from_phone' => $this->fromPhone, 'to_phone' => $this->toPhone, 'profile_name' => $this->profileName, 'wa_id' => $this->waId, 'type' => $this->type, 'body' => $this->body, 'is_button_response' => $this->isButtonResponse, 'button_payload' => $this->buttonPayload, 'button_text' => $this->buttonText, 'original_message_id' => $this->originalMessageId, 'is_reply' => $this->isReply, 'media_count' => $this->mediaCount, 'media' => $this->media, 'latitude' => $this->latitude, 'longitude' => $this->longitude, 'location_address' => $this->locationAddress, 'location_name' => $this->locationName, 'interactive_type' => $this->interactiveType, 'list_reply_id' => $this->listReplyId, 'list_reply_title' => $this->listReplyTitle, 'timestamp' => $this->timestamp, 'account_id' => $this->accountId, 'external_user_id' => $this->externalUserId, 'is_bsuid' => $this->isBsuid, 'raw_payload' => $this->rawPayload, ]; } /** * Check if message has media * * @return bool */ public function hasMedia(): bool { return $this->mediaCount > 0 || !empty($this->media); } /** * Check if message has location * * @return bool */ public function hasLocation(): bool { return $this->latitude !== null && $this->longitude !== null; } /** * Get the message type for database storage * * @return string */ public function getMessageType(): string { if ($this->isButtonResponse) { return self::TYPE_BUTTON_RESPONSE; } if ($this->hasLocation()) { return self::TYPE_LOCATION; } if ($this->hasMedia()) { return self::TYPE_MEDIA; } if ($this->interactiveType) { return self::TYPE_INTERACTIVE; } return self::TYPE_TEXT; } }