├── .github
└── workflows
│ └── php.yml
├── .gitignore
├── README.md
├── composer.json
├── composer.lock
├── config
└── cloudapi.php
├── database
└── migrations
│ ├── 2024_02_10_174929_create_wabas_table.php
│ ├── 2024_02_10_174934_create_waba_phones_table.php
│ ├── 2024_02_10_220808_create_templates_table.php
│ ├── 2024_02_11_220104_create_chat_categories_table.php
│ ├── 2024_02_11_220404_create_chats_table.php
│ ├── 2024_02_11_220525_create_messages_table.php
│ └── 2024_03_06_141423_create_campaigns_table.php
├── docker-compose.yml
├── dockerfile
├── phpunit.xml
├── routes
├── api.php
└── channels.php
├── src
├── Console
│ └── Commands
│ │ ├── CopyMessengerCommand.php
│ │ └── InstallCommand.php
├── Events
│ └── NewWhatsappMessageHook.php
├── Facade.php
├── Factories
│ ├── CampaignFactory.php
│ ├── ChatFactory.php
│ ├── MessageFactory.php
│ ├── TemplateFactory.php
│ ├── WabaFactory.php
│ └── WabaPhoneFactory.php
├── Http
│ ├── Controllers
│ │ ├── APIResourceController.php
│ │ ├── CampaignController.php
│ │ ├── ChatController.php
│ │ ├── MessageController.php
│ │ ├── TemplateController.php
│ │ ├── WabaController.php
│ │ ├── WabaPhoneController.php
│ │ └── WebhookController.php
│ ├── Requests
│ │ ├── SendTemplateRequest.php
│ │ ├── StoreCampaignRequest.php
│ │ └── StoreTemplateRequest.php
│ └── Resources
│ │ ├── MessageResource.php
│ │ └── TemplateResource.php
├── Jobs
│ └── SendToOpenApi.php
├── Lib
│ ├── Message
│ │ ├── BussinessProfile.php
│ │ ├── ProcessMessageWebhook.php
│ │ ├── ReceivedMessage.php
│ │ ├── ReceivedMessageStatus.php
│ │ └── SendMessage.php
│ └── Template
│ │ ├── Adapter
│ │ ├── AppToMeta.php
│ │ ├── AppToResponse.php
│ │ ├── MetaToApp.php
│ │ └── RequestToMeta.php
│ │ ├── CreateTemplate.php
│ │ ├── ProcessStatusWebhook.php
│ │ └── SendTemplate.php
├── Listeners
│ └── ResponseMessageReady.php
├── Models
│ ├── Campaign.php
│ ├── Chat.php
│ ├── ChatCategory.php
│ ├── Message.php
│ ├── Template.php
│ ├── Waba.php
│ └── WabaPhone.php
├── ServiceProvider.php
├── Services
│ ├── FacebookService.php
│ ├── FileManager.php
│ ├── MediaManagerService.php
│ ├── MessageService.php
│ ├── ResumableUploadAPI.php
│ ├── TemplateManagerService.php
│ └── WabaManagerService.php
└── WhatsappCloudApi.php
├── stubs
├── 10
│ └── app
│ │ └── Exceptions
│ │ └── Handler.php
├── 11
│ └── bootstrap
│ │ └── app.php
├── common
│ └── docker-compose.yml
└── interface
│ ├── postcss.config.js
│ ├── resources
│ ├── css
│ │ └── app.css
│ ├── js
│ │ ├── app.js
│ │ └── components
│ │ │ └── Messenger
│ │ │ ├── Chat
│ │ │ ├── ChatBubble.vue
│ │ │ ├── Messenger.vue
│ │ │ ├── NewCampaign.vue
│ │ │ ├── NewTemplateMessage.vue
│ │ │ ├── SendMediaMessage.vue
│ │ │ ├── SendNew.vue
│ │ │ └── Template.vue
│ │ │ ├── Template
│ │ │ ├── Create.vue
│ │ │ ├── Detail.vue
│ │ │ ├── Forms
│ │ │ │ ├── Body.vue
│ │ │ │ ├── Buttons.vue
│ │ │ │ ├── Buttons
│ │ │ │ │ ├── DisableMarketing.vue
│ │ │ │ │ ├── PhoneNumber.vue
│ │ │ │ │ ├── QuickReply.vue
│ │ │ │ │ └── Url.vue
│ │ │ │ ├── Card.vue
│ │ │ │ ├── Cards.vue
│ │ │ │ ├── Footer.vue
│ │ │ │ ├── Form.vue
│ │ │ │ ├── Header.vue
│ │ │ │ └── Waba.vue
│ │ │ ├── Index.vue
│ │ │ ├── Preview
│ │ │ │ └── Index.vue
│ │ │ └── Update.vue
│ │ │ └── Ui
│ │ │ └── Breadcrumb.vue
│ └── views
│ │ ├── messenger.blade.php
│ │ └── template
│ │ ├── create.blade.php
│ │ ├── edit.blade.php
│ │ ├── index.blade.php
│ │ └── show.blade.php
│ ├── tailwind.config.js
│ └── vite.config.js
└── tests
├── Fake
├── Message
│ ├── FakeMessageCreteResponse.php
│ ├── FakeMessageHook.php
│ └── FakeMessageRequests.php
├── Template
│ ├── FakeCreateTemplate.php
│ └── FakeTemplateToMetaFormat.php
├── Waba
│ └── FakeWabaResponses.php
└── Webhook
│ ├── FakeReceivedMessage.php
│ ├── FakeTemplateStatusWebhook.php
│ └── FakeWebhook.php
├── Feature
├── Campaign
│ └── CreateCampaignTest.php
├── Chat
│ └── ChatTest.php
├── Commands
│ ├── CopyMessengerCommandTest.php
│ └── InstallTest.php
├── Message
│ ├── MessageStatusWebhookTest.php
│ ├── MessageTest.php
│ ├── ReceivedMessageTest.php
│ ├── SendMessagesTest.php
│ └── WabaPhoneTest.php
├── Services
│ ├── FileManagerTest.php
│ ├── MediaManagerServiceTest.php
│ └── files
│ │ └── .gitignore
├── Template
│ ├── CreateTemplateTest.php
│ ├── SendTemplateTest.php
│ ├── TemplateStatusWebhookTest.php
│ └── TemplateTest.php
└── Waba
│ ├── WabaManagerTest.php
│ ├── WabaTest.php
│ └── WebhookSubscribeTest.php
└── TestCase.php
/.github/workflows/php.yml:
--------------------------------------------------------------------------------
1 | name: PHP Composer
2 |
3 | on:
4 | push:
5 | branches: [ "develop" ]
6 | pull_request:
7 | branches: [ "develop" ]
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 |
20 | - name: Setup PHP
21 | uses: shivammathur/setup-php@v2
22 | with:
23 | php-version: '8.2'
24 |
25 | - name: Validate composer.json and composer.lock
26 | run: composer validate --strict
27 |
28 | - name: Cache Composer packages
29 | id: composer-cache
30 | uses: actions/cache@v4
31 | with:
32 | path: vendor
33 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
34 | restore-keys: |
35 | ${{ runner.os }}-php-
36 |
37 | - name: Install dependencies
38 | run: composer install --prefer-dist --no-progress
39 |
40 | - name: Check code style
41 | run: ./vendor/bin/pint --test
42 |
43 | - name: Run phpunit Tests
44 | run: |
45 | vendor/bin/phpunit --coverage-clover build/logs/clover.xml
46 |
47 | - name: Upload coverage results to Coveralls
48 | env:
49 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
50 | run: |
51 | vendor/bin/coveralls.php --coverage_clover=build/logs/clover.xml -v
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | html
3 | .phpunit.result.cache
4 | .phpunit.cache
5 | tests/files
6 | tests/logs
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | SDK Consultoría - Whatsapp Cloud API
2 | ====
3 |
4 | Descripción
5 | ------------
6 | ##### ¿Qué puedo esperar de esta librería?
7 | - Conexión WhatsApp cloud API
8 | - Conexión WhatsApp bussines manager
9 | - Chat en tiempo real con WhatsApp
10 | - Envíos y recepción de mensajes de video, imagen, texto, pdf, stickers y reacciones.
11 | - Envíos masivos de whatsapp
12 | - Envíos de plantillas
13 |
14 | Este paquete está en desarrollo, todavía no es una versión estable ni optimizada.
15 |
16 | Video Demo
17 | - https://youtu.be/Tw5X-AVTMa0
18 |
19 | Video envíos masivos de whatsapp
20 | - https://youtu.be/x2nPfEt6HYw
21 |
22 | Video envíos y recepción de mensajes de video, imagen, texto, pdf, stickers y reacciones.
23 | - https://youtu.be/BZUQSeo7yz0
24 |
25 | Tutorial de instalación
26 | - https://youtu.be/EUyvuYIFRz8
27 |
28 | ##### Coverage
29 |
30 | https://coveralls.io/github/sdkconsultoria/whatsapp-cloud-api
31 |
32 | [](https://coveralls.io/github/sdkconsultoria/whatsapp-cloud-api?branch=develop)
33 |
34 | Instalación
35 | ------------
36 | Ejecuta el comando para instalar la librería en tu proyecto Laravel
37 |
38 | ```
39 | composer require sdkconsultoria/whatsapp-cloud-api
40 | ```
41 |
42 | Ejecutar las migraciones, para generar las tablas donde se guardaran los chats
43 | ```
44 | php artisan migrate
45 | ```
46 |
47 | Linkear el storage público para poder obtener la URL de los archivos recibidos y enviados
48 |
49 | ```
50 | php artisan storage:link
51 | ```
52 |
53 | Si quieres usar el Messenger en VUE (opcional), también puedes usar los endpoints
54 | ```
55 | php artisan sdk:whatsapp-messenger-install
56 | ```
57 |
58 | Configuración con Laravel sail y soketi
59 | ------------
60 |
61 | socketi es una opción Open-source para notificaciones push totalmente compatible con pusher y Laravel echo https://docs.soketi.app/
62 |
63 | la configuración por defecto para soketi es esta:
64 | ```
65 | PUSHER_APP_ID=app-id
66 | PUSHER_APP_KEY=app-key
67 | PUSHER_APP_SECRET=app-secret
68 | PUSHER_HOST=soketi
69 | PUSHER_PORT=6001
70 | PUSHER_SCHEME=http
71 | PUSHER_APP_CLUSTER=mt1
72 |
73 | VITE_APP_NAME="${APP_NAME}"
74 | VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
75 | VITE_PUSHER_HOST=localhost
76 | VITE_PUSHER_PORT="${PUSHER_PORT}"
77 | VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
78 | VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
79 |
80 | META_WEBHOOK_TOKEN=
81 | META_TOKEN=
82 | META_APP_ID=
83 | ```
84 |
85 | Configuración de Whatsapp Business Account WABA
86 | ------------
87 |
88 | Puedes obtener la información desde
89 |
90 | https://developers.facebook.com/apps/
91 |
92 | Si no tienes una APP debes crear una en la página de facebook developers.
93 | https://developers.facebook.com/apps
94 |
95 | ###### Registrar el webhook en meta
96 | {{HOST}}/api/v1/whatsapp-webhook
97 |
98 | ###### Obtener waba, plantillas, números telefónicos y bussiness profile desde Meta
99 | {{HOST}}/api/v1/waba/{{wabaId}}/init
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sdkconsultoria/whatsapp-cloud-api",
3 | "type": "library",
4 | "description": "Comunicacion rapida y segura con la API de Whatsapp Cloud",
5 | "keywords": [
6 | "laravel",
7 | "package",
8 | "whatsapp cloud api",
9 | "whatsapp"
10 | ],
11 | "license": "MIT",
12 | "authors": [{
13 | "name": "Camilo Antonio Rodríguez Cruz",
14 | "email": "camilo@sdkconsultoria.com",
15 | "homepage": "https://sdkconsultoria.com",
16 | "role": "Developer"
17 | }],
18 | "require": {
19 | "php": "^8.0",
20 | "guzzlehttp/guzzle": "^7.8",
21 | "pusher/pusher-php-server": "^7.2",
22 | "sdkconsultoria/open-ai-api": "dev-develop"
23 | },
24 | "require-dev": {
25 | "phpunit/phpunit": "^11.0.6",
26 | "orchestra/testbench": "^9.0",
27 | "fakerphp/faker": "^1.23",
28 | "laravel/pint": "^1.14",
29 | "php-coveralls/php-coveralls": "^0.1.0"
30 | },
31 | "autoload": {
32 | "files": [],
33 | "psr-4": {
34 | "Sdkconsultoria\\WhatsappCloudApi\\": "src/",
35 | "Sdkconsultoria\\WhatsappCloudApi\\Tests\\": "tests/"
36 | }
37 | },
38 | "scripts": {
39 | "format": "vendor/bin/pint --config pint.json",
40 | "test": "vendor/bin/phpunit",
41 | "coverage": "vendor/bin/phpunit --coverage-html html"
42 | },
43 | "extra": {
44 | "laravel": {
45 | "providers": ["Sdkconsultoria\\WhatsappCloudApi\\ServiceProvider"],
46 | "aliases": {
47 | "WhatsappCloudApi": "Sdkconsultoria\\WhatsappCloudApi\\Facade"
48 | }
49 | }
50 | },
51 | "minimum-stability": "dev",
52 | "prefer-stable": true
53 | }
54 |
--------------------------------------------------------------------------------
/config/cloudapi.php:
--------------------------------------------------------------------------------
1 |
2 | env('META_TOKEN'),
12 | 'webhook_token' => env('META_WEBHOOK_TOKEN'),
13 | 'webhook_redirect' => env('META_WEBHOOK_REDIRECT'),
14 | 'api_version' => env('WHATSAPP_API_VERSION', 'v19.0'),
15 | 'app_id' => env('META_APP_ID'),
16 | 'app_secret' => env('META_APP_SECRET'),
17 | 'webhook_verify_signature' => env('META_WEBHOOK_VERIFY_SIGNATURE', false),
18 | ];
19 |
--------------------------------------------------------------------------------
/database/migrations/2024_02_10_174929_create_wabas_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->timestamps();
19 | $table->softDeletes();
20 | $table->foreignId('created_by')->nullable()->constrained('users');
21 | $table->foreignId('updated_by')->nullable()->constrained('users');
22 | $table->string('waba_id');
23 | $table->string('name');
24 | $table->string('timezone_id');
25 | $table->string('message_template_namespace');
26 | $table->string('currency');
27 | $table->smallInteger('status')->default('20');
28 | });
29 | }
30 |
31 | /**
32 | * Reverse the migrations.
33 | *
34 | * @return void
35 | */
36 | public function down()
37 | {
38 | Schema::dropIfExists('wabas');
39 | }
40 | };
41 |
--------------------------------------------------------------------------------
/database/migrations/2024_02_10_174934_create_waba_phones_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->timestamps();
19 | $table->softDeletes();
20 | $table->foreignId('created_by')->nullable()->constrained('users');
21 | $table->foreignId('updated_by')->nullable()->constrained('users');
22 | $table->foreignId('waba_id');
23 |
24 | $table->string('address')->nullable();
25 | $table->text('description')->nullable();
26 | $table->string('vertical')->nullable();
27 | $table->text('about')->nullable();
28 | $table->string('email')->nullable();
29 | $table->string('websites')->nullable();
30 | $table->string('profile_picture_url')->nullable();
31 | $table->string('messaging_product')->nullable();
32 |
33 | $table->string('name');
34 | $table->string('code_verification_status')->nullable();
35 | $table->string('display_phone_number');
36 | $table->string('phone_number_clean');
37 | $table->string('quality_rating');
38 | $table->string('phone_id');
39 |
40 | $table->string('pin')->nullable();
41 | $table->smallInteger('status')->default('20');
42 | });
43 | }
44 |
45 | /**
46 | * Reverse the migrations.
47 | *
48 | * @return void
49 | */
50 | public function down()
51 | {
52 | Schema::dropIfExists('waba_phones');
53 | }
54 | };
55 |
--------------------------------------------------------------------------------
/database/migrations/2024_02_10_220808_create_templates_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->timestamps();
19 | $table->softDeletes();
20 | $table->foreignId('created_by')->nullable()->constrained('users');
21 | $table->foreignId('updated_by')->nullable()->constrained('users');
22 | $table->foreignId('waba_id');
23 | $table->string('name');
24 | $table->string('status');
25 | $table->string('category');
26 | $table->string('language');
27 | $table->string('type')->default('GENERIC');
28 | $table->string('template_id');
29 | // $table->smallInteger('status')->default('20');
30 | $table->json('content');
31 | });
32 | }
33 |
34 | /**
35 | * Reverse the migrations.
36 | *
37 | * @return void
38 | */
39 | public function down()
40 | {
41 | Schema::dropIfExists('templates');
42 | }
43 | };
44 |
--------------------------------------------------------------------------------
/database/migrations/2024_02_11_220104_create_chat_categories_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->timestamps();
19 | $table->softDeletes();
20 | $table->foreignId('created_by')->nullable()->constrained('users');
21 | $table->foreignId('updated_by')->nullable()->constrained('users');
22 | $table->string('name');
23 | $table->smallInteger('status')->default('20');
24 | });
25 | }
26 |
27 | /**
28 | * Reverse the migrations.
29 | *
30 | * @return void
31 | */
32 | public function down()
33 | {
34 | Schema::dropIfExists('chat_categories');
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/database/migrations/2024_02_11_220404_create_chats_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->timestamps();
19 | $table->softDeletes();
20 | $table->timestamp('last_message')->nullable();
21 | $table->foreignId('created_by')->nullable()->constrained('users');
22 | $table->foreignId('updated_by')->nullable()->constrained('users');
23 | $table->foreignId('category_id')->nullable();
24 | $table->smallInteger('unread_messages')->nullable()->default('0');
25 | $table->string('order')->nullable();
26 | $table->string('color')->nullable();
27 | $table->foreignId('waba_phone_id');
28 | $table->string('waba_phone');
29 | $table->string('client_phone');
30 | $table->smallInteger('status')->default('20');
31 | $table->string('expiration_timestamp')->nullable();
32 | $table->string('origin')->nullable();
33 | $table->boolean('bot')->default(false);
34 | });
35 | }
36 |
37 | /**
38 | * Reverse the migrations.
39 | *
40 | * @return void
41 | */
42 | public function down()
43 | {
44 | Schema::dropIfExists('chats');
45 | }
46 | };
47 |
--------------------------------------------------------------------------------
/database/migrations/2024_02_11_220525_create_messages_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->foreignId('created_by')->nullable()->constrained('users');
19 | $table->foreignId('chat_id');
20 | $table->string('timestamp')->nullable();
21 | $table->string('sent_at')->nullable();
22 | $table->string('delivered_at')->nullable();
23 | $table->string('read_at')->nullable();
24 | $table->string('message_id')->nullable();
25 | $table->string('type');
26 | $table->string('direction');
27 | $table->string('sended_by')->nullable();
28 | $table->json('body')->nullable();
29 | $table->smallInteger('status')->default('20');
30 | $table->foreignId('response_to')->nullable()->constrained('messages');
31 | $table->string('reaction')->nullable();
32 | });
33 | }
34 |
35 | /**
36 | * Reverse the migrations.
37 | *
38 | * @return void
39 | */
40 | public function down()
41 | {
42 | Schema::dropIfExists('messages');
43 | }
44 | };
45 |
--------------------------------------------------------------------------------
/database/migrations/2024_03_06_141423_create_campaigns_table.php:
--------------------------------------------------------------------------------
1 | id();
16 | $table->timestamps();
17 | $table->smallInteger('status')->default('20');
18 | $table->foreignId('template_id');
19 | $table->foreignId('waba_phone_id');
20 | $table->string('name');
21 | $table->integer('total_messages');
22 | $table->integer('total_sent')->default(0);
23 | $table->integer('total_delivered')->default(0);
24 | $table->integer('total_read')->default(0);
25 | $table->integer('total_error')->default(0);
26 | });
27 | }
28 |
29 | /**
30 | * Reverse the migrations.
31 | */
32 | public function down(): void
33 | {
34 | Schema::dropIfExists('campaigns');
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | php:
3 | build:
4 | context: ./
5 | dockerfile: Dockerfile
6 | image: hellsythe/php-composer
7 | volumes:
8 | - '.:/app'
9 | networks:
10 | - services
11 | networks:
12 | services:
13 | driver: bridge
14 |
--------------------------------------------------------------------------------
/dockerfile:
--------------------------------------------------------------------------------
1 | FROM php:8.2-cli
2 |
3 | RUN apt-get update && apt-get install -y \
4 | libfreetype-dev \
5 | libjpeg62-turbo-dev \
6 | libpng-dev \
7 | && docker-php-ext-configure gd --with-freetype --with-jpeg \
8 | && docker-php-ext-install -j$(nproc) gd
9 |
10 | RUN curl -sS https://getcomposer.org/installer -o composer-setup.php && \
11 | php composer-setup.php --install-dir=/usr/local/bin --filename=composer && \
12 | composer clear-cache
13 |
14 | Run apt install zip -y
15 |
16 | WORKDIR /app
17 |
18 | ENTRYPOINT ["tail", "-f", "/dev/null"]
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ./tests/
18 | ./tests/TestCase.php
19 | ./tests/Fake/
20 |
21 |
22 |
23 |
24 | src
25 |
26 |
27 |
--------------------------------------------------------------------------------
/routes/api.php:
--------------------------------------------------------------------------------
1 | prefix('api/v1')
18 | ->group(function () {
19 | Route::get('whatsapp-webhook', 'WebhookController@subscribe')->name('meta.webhook.subscribe');
20 | Route::post('whatsapp-webhook', 'WebhookController@webhook')->name('meta.webhook');
21 | });
22 |
23 | Route::namespace('Sdkconsultoria\WhatsappCloudApi\Http\Controllers')
24 | ->prefix('api/v1')
25 | ->group(WhatsappCloudApi::routes());
26 |
--------------------------------------------------------------------------------
/routes/channels.php:
--------------------------------------------------------------------------------
1 | confirm('Esto sobrescribira el messenger del paquete usando el messenger del proyecto principal, estas seguro de continuar?')) {
42 | $this->copyMessenger();
43 | $this->comment('SDK Whatsapp Messenger se copio correctamente.');
44 | }
45 | }
46 |
47 | /**
48 | * Cuando estamos desarrollando nueva funcionalidad en el messenger, necesitamos copiar el messenger del proyecto principal al paquete.
49 | */
50 | private function copyMessenger(): void
51 | {
52 | $this->info('Copiando messenger a los stubs del packate...');
53 | $proyectPath = base_path('resources/js/components/Messenger');
54 | $packagePath = __DIR__.'/../../../stubs/interface/resources/js/components/Messenger';
55 | resolve(Filesystem::class)->deleteDirectory($packagePath);
56 | resolve(Filesystem::class)->copyDirectory($proyectPath, $packagePath);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Events/NewWhatsappMessageHook.php:
--------------------------------------------------------------------------------
1 |
29 | */
30 | public function broadcastOn(): array
31 | {
32 | return [
33 | new Channel('new_whatsapp_message'),
34 | ];
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Facade.php:
--------------------------------------------------------------------------------
1 | WabaPhone::factory(),
28 | 'waba_phone' => $this->faker->numberBetween(1111111),
29 | 'client_phone' => $this->faker->numberBetween(1111111),
30 |
31 | 'status' => 20,
32 | 'template_id' => Template::factory(),
33 | 'name' => $this->faker->word,
34 | 'total_messages' => $this->faker->numberBetween(1111111),
35 | 'total_sent' => 0,
36 | 'total_delivered' => 0,
37 | 'total_read' => 0,
38 | 'total_error' => 0,
39 | ];
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Factories/ChatFactory.php:
--------------------------------------------------------------------------------
1 | WabaPhone::factory(),
27 | 'waba_phone' => $this->faker->numberBetween(1111111),
28 | 'client_phone' => $this->faker->numberBetween(1111111),
29 | ];
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Factories/MessageFactory.php:
--------------------------------------------------------------------------------
1 | Chat::factory(),
27 | 'timestamp' => time(),
28 | 'sent_at' => null,
29 | 'delivered_at' => null,
30 | 'read_at' => null,
31 | 'message_id' => 'wamid.'.$this->faker->numberBetween(1111111),
32 | 'type' => 'text',
33 | 'direction' => 'toApp',
34 | 'body' => '{"text": {"body": "Todo bien", "preview_url": false}, "type": "text"}',
35 | 'status' => 20,
36 | 'response_to' => null,
37 | 'reaction' => null,
38 | ];
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Factories/TemplateFactory.php:
--------------------------------------------------------------------------------
1 | [
27 | 'text' => 'Welcome',
28 | ],
29 | ];
30 |
31 | return [
32 | 'waba_id' => Waba::factory(),
33 | 'name' => $this->faker->word,
34 | 'status' => 'APPROVED',
35 | 'category' => 'UTILITY',
36 | 'language' => 'es_MX',
37 | 'template_id' => $this->faker->numberBetween(111111111),
38 | 'content' => json_encode(['components' => $content]),
39 | ];
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Factories/WabaFactory.php:
--------------------------------------------------------------------------------
1 | $this->faker->word,
26 | 'timezone_id' => $this->faker->word,
27 | 'currency' => $this->faker->word,
28 | 'message_template_namespace' => $this->faker->word,
29 | 'waba_id' => $this->faker->numberBetween(111111111),
30 | ];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Factories/WabaPhoneFactory.php:
--------------------------------------------------------------------------------
1 | Waba::factory(),
27 | 'address' => $this->faker->word,
28 | 'description' => $this->faker->word,
29 | 'vertical' => $this->faker->word,
30 | 'about' => $this->faker->word,
31 | 'email' => $this->faker->word,
32 | 'websites' => $this->faker->word,
33 | 'profile_picture_url' => $this->faker->word,
34 | 'name' => $this->faker->word,
35 | 'code_verification_status' => $this->faker->word,
36 | 'display_phone_number' => $this->faker->word,
37 | 'phone_number_clean' => $this->faker->word,
38 | 'quality_rating' => $this->faker->word,
39 | 'phone_id' => $this->faker->word,
40 | ];
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Http/Controllers/APIResourceController.php:
--------------------------------------------------------------------------------
1 | filters() as $index => $filter) {
31 | $search_value = $request->input($index);
32 | if ($search_value) {
33 | $query = $filter($query, $search_value);
34 | }
35 | }
36 |
37 | return $query;
38 | }
39 |
40 | public function index(Request $request)
41 | {
42 | $models = new $this->resource;
43 | $models = $this->applyFilters($models, $request);
44 | $models = $this->defaultOptions($models, $request);
45 | $models = $models->simplePaginate($this->perPage)->appends(request()->except('page'));
46 |
47 | if ($this->transformer) {
48 | $transformer = $this->transformer;
49 |
50 | return $transformer::collection($this->reverseElements($models));
51 | }
52 |
53 | return response()->json($models);
54 | }
55 |
56 | protected function reverseElements($models)
57 | {
58 | if ($this->isReverseElements) {
59 | return $models->reverse();
60 | }
61 |
62 | return $models;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Http/Controllers/CampaignController.php:
--------------------------------------------------------------------------------
1 | file->getRealPath());
16 | $phones = explode(',', $file);
17 |
18 | $campaign = new Campaign();
19 | $campaign->name = $request->name;
20 | $campaign->template_id = $request->template_id;
21 | $campaign->waba_phone_id = $request->waba_phone_id;
22 | $campaign->total_messages = count($phones);
23 | $campaign->save();
24 |
25 | foreach ($phones as $phone) {
26 | resolve(SendTemplate::class)->send($campaign->wabaPhone, $campaign->template, $phone, $request->vars ?? []);
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Http/Controllers/ChatController.php:
--------------------------------------------------------------------------------
1 | orderBy('last_message', 'desc');
15 |
16 | return $models;
17 | }
18 |
19 | protected function filters(): array
20 | {
21 | return [
22 | 'client_phone' => function ($query, $value) {
23 | return $query->where('client_phone', 'like', "%$value%");
24 | },
25 | ];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Http/Controllers/MessageController.php:
--------------------------------------------------------------------------------
1 | where('type', '!=', 'reaction');
22 | $models->orderBy('timestamp', 'desc');
23 |
24 | return $models;
25 | }
26 |
27 | protected function filters(): array
28 | {
29 | return [
30 | 'chat_id' => function ($query, $value) {
31 | return $query->where('chat_id', "$value");
32 | },
33 | ];
34 | }
35 |
36 | public function index(Request $request)
37 | {
38 | $models = new $this->resource;
39 | $models = $this->applyFilters($models, $request);
40 | $models = $this->defaultOptions($models, $request);
41 | $models = $models->simplePaginate()->appends(request()->except('page'));
42 |
43 | $chat = Chat::where('id', $request->input('chat_id'))->first();
44 | $chat->unread_messages = 0;
45 | $chat->save();
46 |
47 | return $this->transformer::collection($this->reverseElements($models));
48 | }
49 |
50 | public function sendMessage(Request $request)
51 | {
52 | $message = resolve(SendMessage::class)->send($request->all());
53 |
54 | return response()->json($message);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Http/Controllers/TemplateController.php:
--------------------------------------------------------------------------------
1 | function ($query, $value) {
23 | return $query->where('status', "$value");
24 | },
25 | 'name' => function ($query, $value) {
26 | return $query->where('name', 'like', "%$value%");
27 | },
28 | ];
29 | }
30 |
31 | public function store(StoreTemplateRequest $request)
32 | {
33 | $template = resolve(CreateTemplate::class)->create($request);
34 |
35 | return response()->json([
36 | 'message' => 'Template created successfully',
37 | 'template' => new TemplateResource($template),
38 | ], 201);
39 | }
40 |
41 | public function sendTemplate(SendTemplateRequest $request)
42 | {
43 | $template = Template::find($request->template);
44 | $wabaPhone = WabaPhone::find($request->waba_phone);
45 |
46 | try {
47 | $message = resolve(SendTemplate::class)->send($wabaPhone, $template, $request->to, $request->vars ?? []);
48 | } catch (\Exception $e) {
49 | return response()->json($e->getMessage(), 500);
50 | }
51 |
52 | return response()->json($message);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Http/Controllers/WabaController.php:
--------------------------------------------------------------------------------
1 | getWabaInfo($wabaId);
16 | resolve(WabaManagerService::class)->getPhoneNumbers($wabaId);
17 | $templates = resolve(WabaManagerService::class)->getAllTemplates($wabaId);
18 | $this->saveTemplates($templates, $wabaId);
19 |
20 | return response()->json($templates);
21 | }
22 |
23 | public function loadTemplatesFromWaba(string $wabaId)
24 | {
25 | $templates = resolve(WabaManagerService::class)->getAllTemplates($wabaId);
26 | $this->saveTemplates($templates, $wabaId);
27 |
28 | return response()->json($templates);
29 | }
30 |
31 | private function saveTemplates($templates, $wabaId)
32 | {
33 | foreach ($templates['data'] as $template) {
34 | resolve(MetaToApp::class)->process($template, $wabaId);
35 | }
36 | }
37 |
38 | public function getWabaInfoFromMeta(string $wabaId)
39 | {
40 | return resolve(WabaManagerService::class)->getWabaInfo($wabaId);
41 | }
42 |
43 | public function getWabaPhonesFromMeta(string $wabaId)
44 | {
45 | return resolve(WabaManagerService::class)->getPhoneNumbers($wabaId);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Http/Controllers/WabaPhoneController.php:
--------------------------------------------------------------------------------
1 | function ($query, $value) {
17 | return $query->where('waba_id', "$value");
18 | },
19 | ];
20 | }
21 |
22 | public function getBussinesProfile(string $phoneId)
23 | {
24 | return resolve(BussinessProfile::class)->process($phoneId);
25 | }
26 |
27 | public function setBussinesProfile(string $phoneId, Request $request)
28 | {
29 | return resolve(BussinessProfile::class)->update($phoneId, $request->all());
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Http/Controllers/WebhookController.php:
--------------------------------------------------------------------------------
1 | hub_verify_token === config('meta.webhook_token')) {
16 | return $request->hub_challenge;
17 | }
18 |
19 | return response()->json(['error' => 'Invalid verify token'], 400);
20 | }
21 |
22 | public function webhook(Request $request)
23 | {
24 | $this->verifySignature($request);
25 | $this->redirectRequestIfNeeded($request);
26 | $data = $request->all()['entry'][0]['changes'][0];
27 |
28 | switch ($data['field']) {
29 | case 'messages':
30 | resolve(ProcessMessageWebhook::class)->process($data['value']);
31 | break;
32 | case 'message_template_status_update':
33 | resolve(ProcessStatusWebhook::class)->process($data['value']);
34 | break;
35 | }
36 | }
37 |
38 | private function redirectRequestIfNeeded(Request $request)
39 | {
40 | if (! config('meta.webhook_redirect')) {
41 | return;
42 | }
43 |
44 | Http::post(config('meta.webhook_redirect'), $request->all());
45 | }
46 |
47 | private function verifySignature(Request $request)
48 | {
49 | if (! config('meta.webhook_verify_signature')) {
50 | return;
51 | }
52 |
53 | $secret = config('meta.app_secret');
54 | $signature = $request->header('x-hub-signature') ?? '';
55 | $payload = $request->getContent();
56 | $expected = 'sha1='.hash_hmac('sha1', $payload, $secret);
57 |
58 | if (! hash_equals($expected, $signature)) {
59 | abort(403, 'Invalid signature');
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/Http/Requests/SendTemplateRequest.php:
--------------------------------------------------------------------------------
1 |
22 | */
23 | public function rules(): array
24 | {
25 | $this->getValidationsComponents();
26 |
27 | return array_merge([
28 | 'waba_phone' => 'required',
29 | 'to' => 'required',
30 | 'template' => 'required|integer|exists:Sdkconsultoria\WhatsappCloudApi\Models\Template,id',
31 | 'vars' => 'nullable|array',
32 | ], $this->getValidationsComponents());
33 | }
34 |
35 | private function getValidationsComponents(): array|false
36 | {
37 | $validations = [];
38 | $template = Template::find($this->template);
39 |
40 | if (! $template) {
41 | return [];
42 | }
43 |
44 | foreach ($template->getComponents() as $index => $component) {
45 | switch (strtoupper($index)) {
46 | case 'BODY':
47 | $this->getComponentVarsValidations($component, $validations, 'body');
48 | break;
49 | case 'HEADER':
50 | $this->getHeaderValidations($component, $validations);
51 | break;
52 |
53 | default:
54 | break;
55 | }
56 | }
57 |
58 | return $validations;
59 | }
60 |
61 | private function getComponentVarsValidations(array $component, array &$validations, $type): void
62 | {
63 | $uniques = $this->countUniqueVars($component['text']);
64 |
65 | if ($uniques === 0) {
66 | $validations["vars.$type.parameters"] = 'nullable';
67 |
68 | return;
69 | }
70 |
71 | $validations["vars.$type.parameters"] = 'required|array|size:'.$uniques;
72 |
73 | for ($i = 0; $i < $uniques; $i++) {
74 | $validations["vars.$type.parameters.$i.text"] = 'required|string';
75 | $validations["vars.$type.parameters.$i.type"] = 'required|string';
76 | }
77 | }
78 |
79 | private function countUniqueVars(string $text): int
80 | {
81 | preg_match_all('/{{([0-9]{1,2})}}/', $text, $matches);
82 | $uniques = array_unique(array_map('intval', $matches[1]));
83 |
84 | return count($uniques);
85 | }
86 |
87 | private function getHeaderValidations(array $component, array &$validations): void
88 | {
89 | switch ($component['format']) {
90 | case 'text':
91 | $this->getComponentVarsValidations($component, $validations, 'header');
92 | break;
93 | case 'document':
94 | case 'video':
95 | case 'image':
96 | $validations['vars.header.parameters.0.type'] = 'required|string';
97 | $validations['vars.header.parameters.0.'.$component['format'].'.link'] = 'required|string';
98 | break;
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/Http/Requests/StoreCampaignRequest.php:
--------------------------------------------------------------------------------
1 | |string>
21 | */
22 | public function rules(): array
23 | {
24 | return [
25 | 'name' => 'required|string|max:255',
26 | 'template_id' => 'required|integer|exists:Sdkconsultoria\WhatsappCloudApi\Models\Template,id',
27 | 'waba_phone_id' => 'required|integer|exists:Sdkconsultoria\WhatsappCloudApi\Models\WabaPhone,id',
28 | 'file' => 'required|extensions:csv',
29 | 'vars' => 'nullable|array',
30 | ];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Http/Requests/StoreTemplateRequest.php:
--------------------------------------------------------------------------------
1 |
21 | */
22 | public function rules(): array
23 | {
24 | $rules = [];
25 | $rules = array_merge($rules, $this->basicRules());
26 |
27 | return $rules;
28 | }
29 |
30 | private function basicRules(): array
31 | {
32 | return [
33 | 'waba_id' => 'required',
34 | 'name' => 'required|string|alpha_dash:ascii',
35 | 'category' => ['required', 'string'],
36 | 'language' => 'required|string',
37 | // 'allow_category_change' => 'required|boolean',
38 | 'components' => 'required|array',
39 | ];
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Http/Resources/MessageResource.php:
--------------------------------------------------------------------------------
1 | $this->id,
20 | 'chat_id' => $this->chat_id,
21 | 'type' => $this->type,
22 | 'direction' => $this->direction,
23 | 'content' => $this->getContent(),
24 | 'timestamp' => $this->timestamp,
25 | 'sended_by' => $this->sended_by,
26 | 'reaction' => $this->reaction,
27 | 'sent_at' => $this->sent_at,
28 | 'read_at' => $this->read_at,
29 | 'delivered_at' => $this->delivered_at,
30 | 'response_to' => $this->getResponseTo(),
31 | ];
32 | }
33 |
34 | private function getContent()
35 | {
36 | $body = json_decode($this->body);
37 |
38 | switch ($this->type) {
39 | case 'image':
40 | case 'video':
41 | case 'audio':
42 | case 'sticker':
43 | case 'document':
44 | return [
45 | 'url' => Url::to($body->{$this->type}->url ?? ''),
46 | 'caption' => $body->{$this->type}->caption ?? '',
47 | ];
48 | case 'contacts':
49 | return $body->contacts;
50 | case 'text':
51 | return $body->text->body;
52 | case 'template':
53 | return $body;
54 | }
55 | }
56 |
57 | private function getResponseTo()
58 | {
59 | $body = json_decode($this->body);
60 |
61 | if ($body->context ?? false) {
62 | return $body->context;
63 | }
64 |
65 | return null;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/Http/Resources/TemplateResource.php:
--------------------------------------------------------------------------------
1 | $this->id,
19 | 'name' => $this->name,
20 | 'status' => $this->status,
21 | 'category' => $this->category,
22 | 'language' => $this->language,
23 | 'components' => json_decode($this->content)->components,
24 | ];
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Jobs/SendToOpenApi.php:
--------------------------------------------------------------------------------
1 | chat->bot) {
34 | $assistant = Assistant::first();
35 | $thread = Thread::where('identifier', $this->content['from'])->first();
36 | if (! $thread) {
37 | resolve(ThreadManager::class)->createConversationWithAssistant($this->content['from'], $this->content['text']['body'], $assistant);
38 | } else {
39 | resolve(ThreadManager::class)->addMessage($thread, $this->content['text']['body']);
40 | resolve(ThreadManager::class)->addRunToThread($thread);
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Lib/Message/BussinessProfile.php:
--------------------------------------------------------------------------------
1 | getBussinesProfile($phoneId);
14 |
15 | return $this->saveWabaPhone($phoneId, $bussinesProfile['data'][0]);
16 | }
17 |
18 | private function saveWabaPhone(string $phoneId, array $profile): WabaPhone
19 | {
20 | $wabaPhone = WabaPhone::where('phone_id', $phoneId)->first();
21 | $wabaPhone->about = $profile['about'];
22 | $wabaPhone->address = $profile['address'];
23 | $wabaPhone->description = $profile['description'];
24 | $wabaPhone->email = $profile['email'];
25 | $wabaPhone->profile_picture_url = $profile['profile_picture_url'];
26 | $wabaPhone->websites = json_encode($profile['websites']);
27 | $wabaPhone->vertical = $profile['vertical'];
28 | $wabaPhone->messaging_product = $profile['messaging_product'];
29 | $wabaPhone->save();
30 |
31 | return $wabaPhone;
32 | }
33 |
34 | public function update(string $phoneId, array $profile)
35 | {
36 | if (isset($profile['picture_profile'])) {
37 | $filePath = $profile['picture_profile']->getRealPath();
38 | $handler = resolve(ResumableUploadAPI::class)->uploadFile($filePath);
39 | unset($profile['picture_profile']);
40 | $profile['profile_picture_handle'] = $handler->handler;
41 | }
42 |
43 | resolve(WabaManagerService::class)->setBussinesProfile($phoneId, $profile);
44 |
45 | return $this->process($phoneId);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Lib/Message/ProcessMessageWebhook.php:
--------------------------------------------------------------------------------
1 | process($messageEvent);
12 | }
13 |
14 | if (isset($messageEvent['statuses'])) {
15 | resolve(ReceivedMessageStatus::class)->process($messageEvent);
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Lib/Message/ReceivedMessage.php:
--------------------------------------------------------------------------------
1 | first();
18 |
19 | $content = $messageEvent['messages'][0];
20 | $chat = Chat::findOrCreateChat($content['from'], $wabaPhoneNumber);
21 |
22 | switch ($content['type']) {
23 | case 'unsupported':
24 | case 'contacts':
25 | case 'text':
26 | $this->processIfIsResponse($content);
27 | $this->processTextMessage($chat, $content);
28 | SendToOpenApi::dispatch($chat, $content);
29 | break;
30 | case 'document':
31 | case 'sticker':
32 | case 'video':
33 | case 'audio':
34 | case 'image':
35 | $this->processIfIsResponse($content);
36 | $content[$content['type']]['url'] = $this->saveFile($content[$content['type']], $phoneNumberId, $chat);
37 | $this->processTextMessage($chat, $content);
38 | break;
39 | case 'reaction':
40 | Message::where('message_id', $content['reaction']['message_id'])
41 | ->update(['reaction' => $content['reaction']['emoji']]);
42 | break;
43 | }
44 |
45 | NewWhatsappMessageHook::dispatch(['chat_id' => $chat->id]);
46 | }
47 |
48 | private function processIfIsResponse(array &$content): void
49 | {
50 | if (isset($content['context']) && isset($content['context']['id'])) {
51 | $message = Message::where('message_id', $content['context']['id'])->firstOrFail();
52 | $content['context']['message'] = $message->body;
53 | }
54 | }
55 |
56 | private function processTextMessage(Chat $chat, array $content): void
57 | {
58 | $messageModel = new Message();
59 | $messageModel->chat_id = $chat->id;
60 | $messageModel->message_id = $content['id'];
61 | $messageModel->timestamp = $content['timestamp'];
62 | $messageModel->status = Message::STATUS_DELIVERED;
63 | $messageModel->type = $content['type'];
64 | $messageModel->body = json_encode($content);
65 | $messageModel->direction = 'toApp';
66 | $messageModel->save();
67 | }
68 |
69 | private function saveFile(array $file, string $phoneNumberId, Chat $chat): string
70 | {
71 | $service = resolve(MediaManagerService::class);
72 |
73 | return $service->download($file['id'], $phoneNumberId, "received/$chat->id/{$file['id']}", 'public');
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/Lib/Message/ReceivedMessageStatus.php:
--------------------------------------------------------------------------------
1 | processSent($messageEvent);
14 | break;
15 | case 'delivered':
16 | $this->processDelivered($messageEvent);
17 | break;
18 | case 'read':
19 | $this->processRead($messageEvent);
20 | break;
21 | }
22 | }
23 |
24 | private function processSent($messageEvent): void
25 | {
26 | $message = Message::where('message_id', $messageEvent['statuses'][0]['id'])->firstOrFail();
27 | $message->sent_at = $messageEvent['statuses'][0]['timestamp'];
28 | $message->save();
29 |
30 | $chat = $message->chat;
31 | $chat->expiration_timestamp = $messageEvent['statuses'][0]['conversation']['expiration_timestamp'];
32 | $chat->save();
33 | }
34 |
35 | private function processDelivered($messageEvent): void
36 | {
37 | $message = Message::where('message_id', $messageEvent['statuses'][0]['id'])->firstOrFail();
38 | $message->delivered_at = $messageEvent['statuses'][0]['timestamp'];
39 | $message->save();
40 | }
41 |
42 | private function processRead($messageEvent): void
43 | {
44 | $message = Message::where('message_id', $messageEvent['statuses'][0]['id'])->firstOrFail();
45 | $message->read_at = $messageEvent['statuses'][0]['timestamp'];
46 | $message->save();
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Lib/Message/SendMessage.php:
--------------------------------------------------------------------------------
1 | createNewMessage($request, $sentBy);
19 | $processedMessage = $this->processMessage($messageModel, $request['message']);
20 | $messageResponse = resolve(MessageService::class)
21 | ->sendMessage($messageModel->chat->wabaPhone->phone_id, $request['to'], $processedMessage);
22 | $this->finishMessage($messageModel, $messageResponse, $processedMessage);
23 | }
24 |
25 | private function createNewMessage(array $request, $sentBy = null): Message
26 | {
27 | $phoneNumber = WabaPhone::find($request['waba_phone_id']);
28 | $message = new Message();
29 | $message->status = Message::STATUS_PENDING;
30 | $message->direction = 'toClient';
31 | $message->timestamp = time();
32 | $message->type = $request['message']['type'];
33 | $message->chat_id = $this->getChatId($phoneNumber, $request['to'])->id;
34 | $message->sended_by = $sentBy;
35 | $message->save();
36 |
37 | return $message;
38 | }
39 |
40 | private function processMessage(Message $message, array $request): array
41 | {
42 | $this->processedMessage = $request;
43 |
44 | switch ($request['type']) {
45 | case 'audio':
46 | case 'video':
47 | case 'document':
48 | case 'image':
49 | $url = "sended/{$message->chat->id}/$message->id.{$request[$request['type']]->extension()}";
50 | $request[$request['type']]->storeAs('public', $url);
51 | $media = resolve(MediaManagerService::class)->upload($message->chat->wabaPhone->phone_id, $request[$request['type']]);
52 | $request[$request['type']] = ['id' => $media['id']];
53 |
54 | $this->processedMessage[$request['type']] = ['url' => Storage::disk()->url($url)];
55 | break;
56 | }
57 |
58 | return $request;
59 | }
60 |
61 | private function getChatId($phoneNumber, $to): Chat
62 | {
63 | $chat = Chat::firstOrCreate([
64 | 'waba_phone' => $phoneNumber->phone_number_clean,
65 | 'waba_phone_id' => $phoneNumber->id,
66 | 'client_phone' => $to,
67 | ]);
68 | $chat->last_message = date('Y-m-d H:i:s');
69 | $chat->save();
70 |
71 | return $chat;
72 | }
73 |
74 | private function finishMessage(Message $messageModel, array $messageResponse): void
75 | {
76 | $messageModel->message_id = $messageResponse['messages'][0]['id'];
77 | $messageModel->body = json_encode($this->processedMessage);
78 | $messageModel->save();
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Lib/Template/Adapter/AppToMeta.php:
--------------------------------------------------------------------------------
1 | fixComponents($metaTemplate['components']);
12 | $template = $this->saveTemplate($metaTemplate, $wabaId);
13 |
14 | return $template;
15 | }
16 |
17 | private function fixComponents(array $components): array
18 | {
19 | $fixedComponents = [];
20 |
21 | foreach ($components as $component) {
22 | $fixedComponents[$component['type']] = $component;
23 | unset($fixedComponents[$component['type']]['type']);
24 |
25 | $this->fixSubcomponents($component['type'], $fixedComponents[$component['type']]);
26 | }
27 |
28 | return $fixedComponents;
29 | }
30 |
31 | private function fixSubcomponents(string $type, array &$subcomponents): void
32 | {
33 | switch ($type) {
34 | case 'CAROUSEL':
35 | foreach ($subcomponents['cards'] as $key => $item) {
36 | $subcomponents['cards'][$key]['components'] = $this->fixComponents($item['components']);
37 | }
38 | break;
39 | }
40 | }
41 |
42 | private function saveTemplate(array $metaTemplate, ?string $wabaId = null): Template
43 | {
44 | $template = $this->findOrCreateTemplate($metaTemplate['id'], $wabaId);
45 | $template->status = $metaTemplate['status'];
46 | $template->category = $metaTemplate['category'];
47 | $template->name = $metaTemplate['name'];
48 | $template->language = $metaTemplate['language'];
49 | $template->content = json_encode($metaTemplate);
50 | $template->save();
51 |
52 | return $template;
53 | }
54 |
55 | private function findOrCreateTemplate(string $templateId, ?string $wabaId = null): Template
56 | {
57 | $template = Template::where('template_id', $templateId)->first();
58 |
59 | if (! $template) {
60 | $template = new Template();
61 | $template->template_id = $templateId;
62 | $template->waba_id = $wabaId;
63 | }
64 |
65 | return $template;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/Lib/Template/Adapter/RequestToMeta.php:
--------------------------------------------------------------------------------
1 | getRealPath();
20 | $handler = resolve(ResumableUploadAPI::class)->uploadFile($filePath);
21 | $component['example']['header_handle'] = [$handler->handler];
22 | }
23 |
24 | if ($component['type'] === 'CAROUSEL') {
25 | foreach ($component['cards'] as $cardIndex => $card) {
26 | $subComponents = [];
27 | foreach ($card['components'] as $componentIndex => $subcomponent) {
28 | $subcomponent['type'] = strtoupper($componentIndex);
29 | if ($subcomponent['type'] === 'HEADER'){
30 | $filePath = $subcomponent['example']['header_handle'][0]->getRealPath();
31 | $handler = resolve(ResumableUploadAPI::class)->uploadFile($filePath);
32 | $subcomponent['example']['header_handle'] = [$handler->handler];
33 | }
34 | $subComponents[] = $subcomponent;
35 | }
36 | $component['cards'][$cardIndex]['components'] = $subComponents;
37 | }
38 | }
39 |
40 | return $component;
41 | }, $components, array_keys($components));
42 | $processed['components'] = $components;
43 |
44 | return $processed;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Lib/Template/CreateTemplate.php:
--------------------------------------------------------------------------------
1 | waba_id);
16 | $templateInMetaFormat = resolve(RequestToMeta::class)->process($request->all());
17 | $metaTemplateResponse = resolve(TemplateManagerService::class)->createTemplate($waba->waba_id, $templateInMetaFormat);
18 | $templateFromMeta = resolve(TemplateManagerService::class)->getTemplate($metaTemplateResponse['id']);
19 |
20 | return resolve(MetaToApp::class)->process($templateFromMeta, $waba->id);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Lib/Template/ProcessStatusWebhook.php:
--------------------------------------------------------------------------------
1 | firstOrFail();
13 | $template->status = $messageEvent['event'];
14 | $template->save();
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Lib/Template/SendTemplate.php:
--------------------------------------------------------------------------------
1 | setVars($vars);
16 | $message = resolve(MessageService::class)
17 | ->sendTemplate($wabaPhone, $to, $template);
18 |
19 | $messageModel = new Message();
20 | $messageModel->direction = 'toClient';
21 | $messageModel->body = json_encode($template->componentsWithVars);
22 | $messageModel->timestamp = time();
23 | $messageModel->message_id = $message['messages'][0]['id'];
24 | $messageModel->type = 'template';
25 | $messageModel->chat_id = $this->getChatId($wabaPhone, $to);
26 | $messageModel->sended_by = $sentBy;
27 | $messageModel->save();
28 | }
29 |
30 | private function getChatId($wabaPhone, $to)
31 | {
32 | $chat = Chat::firstOrCreate([
33 | 'waba_phone' => $wabaPhone->phone_number_clean,
34 | 'waba_phone_id' => $wabaPhone->id,
35 | 'client_phone' => $to,
36 | ]);
37 |
38 | return $chat->id;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Listeners/ResponseMessageReady.php:
--------------------------------------------------------------------------------
1 | run->thread->identifier;
24 | $message = $event->messages['data'][0]['content'][0]['text']['value'];
25 |
26 | resolve(SendMessage::class)->send([
27 | 'waba_phone_id' => 1,
28 | 'to' => $identifier,
29 | 'message' => [
30 | 'type' => 'text',
31 | 'text' => [
32 | 'body' => $message,
33 | 'preview_url' => false,
34 | ],
35 | ],
36 | ], 'BOT');
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Models/Campaign.php:
--------------------------------------------------------------------------------
1 | belongsTo(WabaPhone::class);
23 | }
24 |
25 | public function template()
26 | {
27 | return $this->belongsTo(Template::class);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Models/Chat.php:
--------------------------------------------------------------------------------
1 | $wabaPhoneNumber->id,
20 | 'waba_phone' => $wabaPhoneNumber->phone_number_clean,
21 | 'client_phone' => $from,
22 | ]);
23 |
24 | $chat->unread_messages += 1;
25 | $chat->last_message = date('Y-m-d H:i:s');
26 | $chat->status = Chat::STATUS_UNREAD;
27 | $chat->save();
28 |
29 | return $chat;
30 | }
31 |
32 | public function wabaPhone()
33 | {
34 | return $this->belongsTo(WabaPhone::class);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Models/ChatCategory.php:
--------------------------------------------------------------------------------
1 | belongsTo(Chat::class);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Models/Template.php:
--------------------------------------------------------------------------------
1 | content, true)['components'];
25 | }
26 |
27 | public function setVars(array $vars): void
28 | {
29 | $components = $this->getComponents();
30 | $fixedVars = [];
31 | foreach ($vars as $key => $var) {
32 | $key = strtoupper($key);
33 | $fixedVars[] = array_merge($var, ['type' => $key]);
34 | $components[$key] = $components[$key] ?? [];
35 |
36 | switch ($key) {
37 | case 'BODY':
38 | $components[$key] = $this->replaceVars($components[$key], $var);
39 | break;
40 | case 'BUTTON':
41 | $components['BUTTON'] = $components['BUTTON'] ?? [];
42 | $components['BUTTON'][] = $var;
43 | break;
44 |
45 | default:
46 | $components[$key]['parameters'] = $var;
47 | break;
48 | }
49 | }
50 |
51 | $this->componentsWithVars = $components;
52 | $this->vars = $fixedVars;
53 | }
54 |
55 | private function replaceVars(array $component, array $var): array
56 | {
57 | $text = $component['text'];
58 | foreach ($var['parameters'] as $index => $value) {
59 | $text = str_replace('{{'.($index + 1).'}}', $value['text'], $text);
60 | }
61 | $component['text'] = $text;
62 |
63 | return $component;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Models/Waba.php:
--------------------------------------------------------------------------------
1 | first();
15 |
16 | if (! $waba) {
17 | $waba = new Waba();
18 | $waba->waba_id = $wabaInfo['id'];
19 | }
20 |
21 | $waba->waba_id = $wabaInfo['id'];
22 | $waba->name = $wabaInfo['name'];
23 | $waba->timezone_id = $wabaInfo['timezone_id'];
24 | $waba->message_template_namespace = $wabaInfo['message_template_namespace'];
25 | $waba->currency = $wabaInfo['currency'] ?? 'USD';
26 | $waba->save();
27 | }
28 |
29 | public function templates()
30 | {
31 | return $this->hasMany(Template::class, 'waba_id', 'waba_id');
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Models/WabaPhone.php:
--------------------------------------------------------------------------------
1 | first();
17 | $wabaPhone = WabaPhone::where('phone_id', $phone['id'])->first();
18 |
19 | if (! $wabaPhone) {
20 | $wabaPhone = new WabaPhone();
21 | $wabaPhone->phone_id = $phone['id'];
22 | $wabaPhone->waba_id = $waba->id;
23 | }
24 |
25 | $wabaPhone->name = $phone['verified_name'];
26 | $wabaPhone->display_phone_number = $phone['display_phone_number'];
27 | $wabaPhone->quality_rating = $phone['quality_rating'];
28 | $wabaPhone->phone_number_clean = str_replace(['-', ' ', '+'], '', $phone['display_phone_number']);
29 | $wabaPhone->save();
30 |
31 | try {
32 | resolve(BussinessProfile::class)->process($phone['id']);
33 | } catch (\Exception $e) {
34 | \Log::error($e->getMessage());
35 | }
36 |
37 | }
38 | }
39 |
40 | /**
41 | * @codeCoverageIgnore
42 | */
43 | public function waba()
44 | {
45 | return $this->belongsTo(Waba::class);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/ServiceProvider.php:
--------------------------------------------------------------------------------
1 | loadRoutesFrom(__DIR__.'/../routes/api.php');
21 | $this->registerBroadcasting();
22 | $this->registerMigrations();
23 | $this->registerCustomFactory();
24 | $this->registerCommands();
25 | $this->registerRouteMacro();
26 | $this->registerEvents();
27 | }
28 |
29 | private function registerBroadcasting(): void
30 | {
31 | Broadcast::routes();
32 |
33 | require __DIR__.'/../routes/channels.php';
34 | }
35 |
36 | private function registerMigrations()
37 | {
38 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
39 | }
40 |
41 | /**
42 | * @codeCoverageIgnore
43 | */
44 | private function registerCustomFactory(): void
45 | {
46 | Factory::guessFactoryNamesUsing(function (string $model_name) {
47 | $sdk = Str::startsWith($model_name, 'Sdkconsultoria');
48 |
49 | if ($sdk) {
50 | return Str::of($model_name)->replace('Models', 'Factories').'Factory';
51 | }
52 |
53 | $namespace = 'Database\\Factories\\';
54 |
55 | $model_name = str_replace('App\\Models\\', '', $model_name);
56 |
57 | return $namespace.$model_name.'Factory';
58 | });
59 | }
60 |
61 | private function registerCommands(): void
62 | {
63 | if ($this->app->runningInConsole()) {
64 | $this->commands([
65 | \Sdkconsultoria\WhatsappCloudApi\Console\Commands\InstallCommand::class,
66 | \Sdkconsultoria\WhatsappCloudApi\Console\Commands\CopyMessengerCommand::class,
67 | ]);
68 | }
69 | }
70 |
71 | /**
72 | * @codeCoverageIgnore
73 | */
74 | private function registerRouteMacro(): void
75 | {
76 | Route::macro('ResourceView', function ($uri) {
77 | Route::get("{$uri}", fn () => view("{$uri}.index"))->name("{$uri}_view.index");
78 | Route::get("{$uri}/create", fn () => view("{$uri}.create"))->name("{$uri}_view.create");
79 | Route::get("{$uri}/update/{id}", fn () => view("{$uri}.edit"))->name("{$uri}_view.edit");
80 | Route::get("{$uri}/{id}", fn () => view("{$uri}.show"))->name("{$uri}_view.show");
81 | });
82 |
83 | Route::macro('ApiResource', function ($uri, $controller) {
84 | Route::get("{$uri}", "{$controller}@index")->name("{$uri}.index");
85 | Route::post("{$uri}/create", "{$controller}@storage")->name("{$uri}.storage");
86 | Route::put("{$uri}/update/{id}", "{$controller}@update")->name("{$uri}.update");
87 | Route::get("{$uri}/{id}", "{$controller}@show")->name("{$uri}.show");
88 | Route::delete("{$uri}/{id}", "{$controller}@destroy")->name("{$uri}.destroy");
89 | });
90 | }
91 |
92 | private function registerEvents(): void
93 | {
94 | Event::listen(
95 | MessageReady::class,
96 | ResponseMessageReady::class,
97 | );
98 | }
99 |
100 | /**
101 | * Register any application services.
102 | *
103 | * @codeCoverageIgnore
104 | */
105 | public function register(): void
106 | {
107 | $this->mergeConfigFrom(
108 | __DIR__.'/../config/cloudapi.php', 'meta'
109 | );
110 |
111 | $this->app->bind('WhatsappCloudApi', function () {
112 | return new WhatsappCloudApi();
113 | });
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/Services/FacebookService.php:
--------------------------------------------------------------------------------
1 | graph_url .= config('meta.api_version').'/';
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Services/FileManager.php:
--------------------------------------------------------------------------------
1 | graph_url}{$phoneNumberId}/media";
13 |
14 | $response = Http::withToken(config('meta.token'))
15 | ->attach('file', file_get_contents($file->getRealPath()), $file->getClientOriginalName(), ['Content-Type' => $file->getClientMimeType()])
16 | ->post($url, [
17 | 'filename' => $filename,
18 | 'messaging_product' => 'whatsapp',
19 | ]);
20 |
21 | return $response->json();
22 | }
23 |
24 | public function download(string $mediaId, string $phoneNumberId, string $filename, string $disk = 'local'): string
25 | {
26 | $media = $this->getMediaUrl($mediaId, $phoneNumberId);
27 |
28 | return $this->downloadMedia($media['url'], $filename, $disk);
29 | }
30 |
31 | private function getMediaUrl(string $mediaId, string $phoneNumberId): array
32 | {
33 | $url = "{$this->graph_url}{$mediaId}?phone_number_id={$phoneNumberId}";
34 | $response = Http::withToken(config('meta.token'))->get($url);
35 |
36 | return $response->json();
37 | }
38 |
39 | private function downloadMedia(string $mediaUrl, string $filename, string $disk): string
40 | {
41 | $response = Http::withToken(config('meta.token'))->get($mediaUrl);
42 |
43 | $extension = str_replace('inline;filename=File', '', $response->getHeaders()['Content-Disposition'][0]);
44 |
45 | Storage::disk($disk)->put("$filename$extension", $response->getBody()->getContents());
46 |
47 | return Storage::disk($disk)->url("$filename$extension");
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Services/MessageService.php:
--------------------------------------------------------------------------------
1 | graph_url .= "$phoneId/messages";
14 |
15 | $response = Http::withToken(config('meta.token'))->post($this->graph_url, array_merge([
16 | 'messaging_product' => 'whatsapp',
17 | 'recipient_type' => 'individual',
18 | 'to' => $to,
19 | ], $message))->throw();
20 |
21 | return $response->json();
22 | }
23 |
24 | public function sendTemplate(WabaPhone $phone, string $to, Template $template): array
25 | {
26 | $this->graph_url .= "$phone->phone_id/messages";
27 | $response = Http::withToken(config('meta.token'))->post($this->graph_url, [
28 | 'messaging_product' => 'whatsapp',
29 | 'recipient_type' => 'individual',
30 | 'to' => $to,
31 | 'type' => 'template',
32 | 'template' => [
33 | 'name' => $template->name,
34 | 'language' => [
35 | 'code' => $template->language,
36 | ],
37 | 'components' => $template->vars,
38 | ],
39 | ])->throw();
40 |
41 | return $response->json();
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Services/ResumableUploadAPI.php:
--------------------------------------------------------------------------------
1 | validate($file);
23 | $this->createUploadSession();
24 | $this->uploadOrFail();
25 |
26 | return $this;
27 | }
28 |
29 | private function validate(string $file)
30 | {
31 | if (file_exists($file)) {
32 | $this->file = $file;
33 | $this->mime_type = mime_content_type($file);
34 | $this->file_size = filesize($file);
35 | } else {
36 | throw new Exception("El archivo {$file} no existe.");
37 | }
38 | }
39 |
40 | private function createUploadSession(): void
41 | {
42 | $appId = config('meta.app_id');
43 | $url = $this->graph_url."{$appId}/uploads";
44 | $url .= '?file_length='.$this->file_size;
45 | $url .= '&file_type='.$this->mime_type;
46 | $url .= '&file_name=myprofile.jpg';
47 | $url .= '&access_token='.config('meta.token');
48 |
49 | $response = Http::post($url)->throw()->json();
50 | $this->session_id = $response['id'];
51 | }
52 |
53 | private function uploadOrFail(string $offset = '0')
54 | {
55 | $url = $this->graph_url.$this->session_id;
56 |
57 | $response = Http::withHeaders([
58 | 'Authorization' => 'OAuth '.config('meta.token'),
59 | 'file_offset' => $offset,
60 | ])->withBody(file_get_contents($this->file), $this->mime_type)
61 | ->post($url)->throw()->json();
62 |
63 | if (! isset($response['h'])) {
64 | throw new Exception(json_encode($response));
65 | }
66 |
67 | $this->handler = $response['h'];
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Services/TemplateManagerService.php:
--------------------------------------------------------------------------------
1 | graph_url .= $wabaId.'/message_templates';
12 | $response = Http::withToken(config('meta.token'))->post($this->graph_url, $template);
13 |
14 | return $response->json();
15 | }
16 |
17 | public function getTemplate(string $templateId): array
18 | {
19 | $this->graph_url .= $templateId;
20 | $response = Http::withToken(config('meta.token'))->get($this->graph_url);
21 |
22 | return $response->json();
23 | }
24 |
25 | // public function getAllTemplates(string $wabaId): array
26 | // {
27 | // $this->graph_url .= $wabaId;
28 | // $response = Http::withToken(config('meta.token'))->get($this->graph_url);
29 |
30 | // return $response->json();
31 | // }
32 |
33 | // public function setTemplate(string $phoneId): array
34 | // {
35 | // $this->graph_url .= $phoneId.'/whatsapp_business_profile?fields=about,address,description,email,profile_picture_url,websites,vertical';
36 | // $response = Http::withToken(config('meta.token'))->get($this->graph_url);
37 |
38 | // return $response->json();
39 | // }
40 |
41 | // public function deleteTemplate(string $phoneId): array
42 | // {
43 | // $this->graph_url .= $phoneId.'/whatsapp_business_profile?fields=about,address,description,email,profile_picture_url,websites,vertical';
44 | // $response = Http::withToken(config('meta.token'))->get($this->graph_url);
45 |
46 | // return $response->json();
47 | // }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Services/WabaManagerService.php:
--------------------------------------------------------------------------------
1 | graph_url .= $wabaId;
15 | $response = Http::withToken(config('meta.token'))->get($this->graph_url);
16 |
17 | Waba::saveWaba($response->json());
18 |
19 | return $response->json();
20 | }
21 |
22 | public function getPhoneNumbers(string $wabaId): array
23 | {
24 | $this->graph_url .= $wabaId.'/phone_numbers';
25 | $response = Http::withToken(config('meta.token'))->get($this->graph_url);
26 |
27 | WabaPhone::savePhones($response->json(), $wabaId);
28 |
29 | return $response->json();
30 | }
31 |
32 | public function getBussinesProfile(string $phoneId): array
33 | {
34 | $this->graph_url .= $phoneId.'/whatsapp_business_profile?fields=about,address,description,email,profile_picture_url,websites,vertical';
35 | $response = Http::withToken(config('meta.token'))->get($this->graph_url);
36 |
37 | return $response->json();
38 | }
39 |
40 | public function setBussinesProfile(string $phoneId, $data): array
41 | {
42 | $this->graph_url .= $phoneId.'/whatsapp_business_profile';
43 |
44 | $response = Http::withToken(config('meta.token'))
45 | ->post($this->graph_url, array_merge($data, ['messaging_product' => 'whatsapp']));
46 |
47 | return $response->json();
48 | }
49 |
50 | public function getAllTemplates(string $wabaId): array
51 | {
52 | $this->graph_url .= $wabaId.'/message_templates';
53 | $response = Http::withToken(config('meta.token'))->get($this->graph_url);
54 |
55 | return $response->json();
56 | }
57 |
58 | // public function getAllWabas(): Response
59 | // {
60 | // $this->graph_url .= config('meta.app_id') . '/owned_whatsapp_business_accounts';
61 | // $response = Http::withToken(config('meta.token'))->get($this->graph_url);
62 | // return $response->json();
63 | // }
64 |
65 | // public function subscribeWaba(string $wabaId): Response
66 | // {
67 | // $this->graph_url .= $wabaId.'/phone_numbers';
68 | // $response = Http::withToken(config('meta.token'))->get($this->graph_url);
69 | // return $response->json();
70 | // }
71 |
72 | // public function getAllPhoneNumberFromWaba(string $wabaId): Response
73 | // {
74 | // $this->graph_url .= $wabaId.'/phone_numbers';
75 | // $response = Http::withToken(config('meta.token'))->get($this->graph_url);
76 | // return $response->json();
77 | // }
78 |
79 | // public function getPin2faToNumber(string $phoneId): Response
80 | // {
81 | // $this->graph_url .= $phoneId.'/register';
82 | // $response = Http::withToken(config('meta.token'))->post($this->graph_url, [
83 | // 'messaging_product' => 'whatsapp',
84 | // ]);
85 | // return $response->json();
86 | // }
87 | // public function enable2faToNumber(string $phoneId, string $pin): Response
88 | // {
89 | // $this->graph_url .= $phoneId.'/register';
90 | // $response = Http::withToken(config('meta.token'))->post($this->graph_url, [
91 | // 'messaging_product' => 'whatsapp',
92 | // 'pin' => $pin
93 | // ]);
94 | // return $response->json();
95 | // }
96 | }
97 |
--------------------------------------------------------------------------------
/src/WhatsappCloudApi.php:
--------------------------------------------------------------------------------
1 | name('waba.init');
13 | Route::get('waba/{wabaId}/get-templates', 'WabaController@loadTemplatesFromWaba')->name('waba.getTemplatesFromMeta');
14 | Route::get('waba/{wabaId}/get-info', 'WabaController@getWabaInfoFromMeta')->name('waba.getInfoFromMeta');
15 | Route::get('waba/{wabaId}/get-phones', 'WabaController@getWabaPhonesFromMeta')->name('waba.getPhonesFromMeta');
16 |
17 | Route::ApiResource('template', 'TemplateController');
18 | Route::get('waba', 'WabaController@index')->name('waba.index');
19 |
20 | Route::post('message/send', 'MessageController@sendMessage')->name('message.send');
21 | Route::post('message/template/send', 'TemplateController@sendTemplate')->name('message.template.send');
22 | Route::get('message', 'MessageController@index')->name('message.index');
23 |
24 | Route::get('waba-phone', 'WabaPhoneController@index')->name('waba_phone.waba_number');
25 | Route::get('waba-phone/{phoneId}/bussines_profile', 'WabaPhoneController@getBussinesProfile')->name('waba_phone.bussines_profile');
26 | Route::post('waba-phone/{phoneId}/bussines_profile', 'WabaPhoneController@setBussinesProfile')->name('waba_phone.storage_bussines_profile');
27 |
28 | Route::get('chat', 'ChatController@index')->name('chat.index');
29 |
30 | Route::post('campaign', 'CampaignController@store')->name('campaign.store');
31 | };
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/stubs/10/app/Exceptions/Handler.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | protected $dontFlash = [
17 | 'current_password',
18 | 'password',
19 | 'password_confirmation',
20 | ];
21 |
22 | /**
23 | * Register the exception handling callbacks for the application.
24 | */
25 | public function register(): void
26 | {
27 | $this->reportable(function (Throwable $e) {
28 | //
29 | });
30 | }
31 |
32 | /**
33 | * Render an exception into an HTTP response.
34 | *
35 | * @param \Illuminate\Http\Request $request
36 | * @return \Symfony\Component\HttpFoundation\Response
37 | *
38 | * @throws \Throwable
39 | */
40 | public function render($request, Throwable $e)
41 | {
42 |
43 | if ($e instanceof ValidationException && ($request->ajax() || $request->wantsJson() || $request->is('api/*'))) {
44 | return response()->json($e->errors(), 422);
45 | }
46 |
47 | return parent::render($request, $e);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/stubs/11/bootstrap/app.php:
--------------------------------------------------------------------------------
1 | withRouting(
9 | web: __DIR__.'/../routes/web.php',
10 | commands: __DIR__.'/../routes/console.php',
11 | health: '/up',
12 | )
13 | ->withMiddleware(function (Middleware $middleware) {
14 | //
15 | })
16 | ->withExceptions(function (Exceptions $exceptions) {
17 | //
18 | })->create();
19 |
--------------------------------------------------------------------------------
/stubs/common/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | laravel.test:
3 | build:
4 | context: ./vendor/laravel/sail/runtimes/8.3
5 | dockerfile: Dockerfile
6 | args:
7 | WWWGROUP: '${WWWGROUP}'
8 | image: sail-8.3/app
9 | extra_hosts:
10 | - 'host.docker.internal:host-gateway'
11 | ports:
12 | - '${APP_PORT:-80}:80'
13 | - '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
14 | environment:
15 | WWWUSER: '${WWWUSER}'
16 | LARAVEL_SAIL: 1
17 | XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
18 | XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
19 | IGNITION_LOCAL_SITES_PATH: '${PWD}'
20 | volumes:
21 | - '.:/var/www/html'
22 | - './..:/var/www/packages'
23 | networks:
24 | - sail
25 | depends_on:
26 | - mysql
27 | - redis
28 | - meilisearch
29 | - mailpit
30 | - selenium
31 | mysql:
32 | image: 'mysql/mysql-server:8.0'
33 | ports:
34 | - '${FORWARD_DB_PORT:-3306}:3306'
35 | environment:
36 | MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
37 | MYSQL_ROOT_HOST: '%'
38 | MYSQL_DATABASE: '${DB_DATABASE}'
39 | MYSQL_USER: '${DB_USERNAME}'
40 | MYSQL_PASSWORD: '${DB_PASSWORD}'
41 | MYSQL_ALLOW_EMPTY_PASSWORD: 1
42 | volumes:
43 | - 'sail-mysql:/var/lib/mysql'
44 | - './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
45 | networks:
46 | - sail
47 | healthcheck:
48 | test:
49 | - CMD
50 | - mysqladmin
51 | - ping
52 | - '-p${DB_PASSWORD}'
53 | retries: 3
54 | timeout: 5s
55 | redis:
56 | image: 'redis:alpine'
57 | ports:
58 | - '${FORWARD_REDIS_PORT:-6379}:6379'
59 | volumes:
60 | - 'sail-redis:/data'
61 | networks:
62 | - sail
63 | healthcheck:
64 | test:
65 | - CMD
66 | - redis-cli
67 | - ping
68 | retries: 3
69 | timeout: 5s
70 | meilisearch:
71 | image: 'getmeili/meilisearch:latest'
72 | ports:
73 | - '${FORWARD_MEILISEARCH_PORT:-7700}:7700'
74 | environment:
75 | MEILI_NO_ANALYTICS: '${MEILISEARCH_NO_ANALYTICS:-false}'
76 | volumes:
77 | - 'sail-meilisearch:/meili_data'
78 | networks:
79 | - sail
80 | healthcheck:
81 | test:
82 | - CMD
83 | - wget
84 | - '--no-verbose'
85 | - '--spider'
86 | - 'http://localhost:7700/health'
87 | retries: 3
88 | timeout: 5s
89 | mailpit:
90 | image: 'axllent/mailpit:latest'
91 | ports:
92 | - '${FORWARD_MAILPIT_PORT:-1025}:1025'
93 | - '${FORWARD_MAILPIT_DASHBOARD_PORT:-8025}:8025'
94 | networks:
95 | - sail
96 | selenium:
97 | image: selenium/standalone-chrome
98 | extra_hosts:
99 | - 'host.docker.internal:host-gateway'
100 | volumes:
101 | - '/dev/shm:/dev/shm'
102 | networks:
103 | - sail
104 | soketi:
105 | image: 'quay.io/soketi/soketi:latest-16-alpine'
106 | environment:
107 | SOKETI_DEBUG: '1'
108 | SOKETI_METRICS_SERVER_PORT: '9601'
109 | ports:
110 | - '${SOKETI_PORT:-6001}:6001'
111 | - '${SOKETI_METRICS_SERVER_PORT:-9601}:9601'
112 | networks:
113 | - sail
114 | networks:
115 | sail:
116 | driver: bridge
117 | volumes:
118 | sail-mysql:
119 | driver: local
120 | sail-redis:
121 | driver: local
122 | sail-meilisearch:
123 | driver: local
124 |
--------------------------------------------------------------------------------
/stubs/interface/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/stubs/interface/resources/css/app.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/stubs/interface/resources/js/app.js:
--------------------------------------------------------------------------------
1 | import Echo from 'laravel-echo';
2 | import Pusher from 'pusher-js';
3 | import { createApp } from 'vue'
4 | import MessengerComponent from "./components/Messenger/Chat/Messenger.vue";
5 | import TemplateIndex from "./components/Messenger/Template/Index.vue";
6 | import TemplateCreate from "./components/Messenger/Template/Create.vue";
7 | import TemplateUpdate from "./components/Messenger/Template/Update.vue";
8 | import TemplateDetail from "./components/Messenger/Template/Detail.vue";
9 |
10 | window.Pusher = Pusher;
11 |
12 | window.Echo = new Echo({
13 | broadcaster: 'pusher',
14 | key: import.meta.env.VITE_PUSHER_APP_KEY,
15 | cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1',
16 | wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`,
17 | wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
18 | wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
19 | forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https',
20 | enabledTransports: ['ws', 'wss'],
21 | });
22 |
23 | let element = document.getElementById('messenger')
24 | if (element !== null) {
25 | const app = createApp({});
26 | app.component('MessengerComponent', MessengerComponent)
27 | app.component('TemplateIndex', TemplateIndex)
28 | app.component('TemplateCreate', TemplateCreate)
29 | app.component('TemplateUpdate', TemplateUpdate)
30 | app.component('TemplateDetail', TemplateDetail)
31 |
32 | app.mount('#messenger');
33 | }
34 |
35 | // const files = require.context('../myFolder', true, /(Module|Utils)\.js$/)
36 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Chat/ChatBubble.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
{{ message.reaction }}
10 |
{{ message.content }}
11 |
12 | {{ message.content.caption }}
13 |
14 |
15 |
16 |
17 |
18 |
19 | {{ message.content.caption }}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
28 |
29 |
30 |
31 | - Nombre: {{ contact.name.first_name }}
32 | - Telefonos:
33 | {{ phone.phone }}
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
61 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Chat/Messenger.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
20 |
{{ current_conversation.client_phone }}
21 |
22 |
23 |
29 |
30 |
31 |
32 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
132 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Chat/NewCampaign.vue:
--------------------------------------------------------------------------------
1 |
2 | Enviar Plantilla a multiples destinatario
3 |
4 |
5 | Para enviar una plantilla a multiples destinatarios se debe hacer por medio de una campaña
6 |
7 |
8 |
9 |
11 |
12 |
13 |
18 |
19 |
20 |
24 |
25 |
26 |
28 | El archivo debe ser .csv
29 |
30 |
38 |
39 |
40 |
98 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Chat/NewTemplateMessage.vue:
--------------------------------------------------------------------------------
1 |
2 | Enviar Mensaje a un destinatario
3 |
4 |
5 | Recuerda que para iniciar una conversación se debe hacer por medio de una plantilla
6 |
7 |
8 |
9 |
14 |
15 |
16 |
20 |
21 |
22 |
23 |
24 |
28 |
29 |
30 |
31 |
35 |
36 |
37 |
38 |
46 |
47 |
48 |
115 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Chat/SendMediaMessage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
11 |
12 |
13 | -
14 |
15 | Documento
16 |
17 |
18 | -
19 |
20 | Imagen
21 |
22 |
23 | -
24 |
25 | Video
26 |
27 |
28 | -
29 |
30 | Audio
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
60 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Chat/SendNew.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
20 |
21 |
22 |
28 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Chat/Template.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ content.HEADER?.text }}
3 | {{ content.BODY?.text }}
4 | {{ content.FOOTER?.text }}
5 |
6 |
7 |
13 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Template/Create.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
63 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Template/Detail.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
19 |
34 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Template/Forms/Body.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
11 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Template/Forms/Buttons.vue:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
21 | Respuesta Rápida
22 |
23 |
24 |
25 |
26 | Llamada a la acción
27 |
28 |
32 |
33 |
34 |
35 |
66 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Template/Forms/Buttons/DisableMarketing.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
27 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Template/Forms/Buttons/PhoneNumber.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
27 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Template/Forms/Buttons/QuickReply.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
23 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Template/Forms/Buttons/Url.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
34 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Template/Forms/Card.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
26 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Template/Forms/Cards.vue:
--------------------------------------------------------------------------------
1 |
2 |
27 |
28 |
64 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Template/Forms/Footer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Template/Forms/Form.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 |
13 |
14 |
15 |
17 |
18 |
19 |
20 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
49 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Template/Forms/Header.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
15 |
16 |
17 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
50 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Template/Forms/Waba.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
26 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Template/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
6 | Platillas de whatsapp
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
22 |
23 |
33 |
34 |
35 |
36 |
37 | |
38 | Nombre |
39 | Categoría |
40 | Idioma |
41 | Estatus |
42 |
43 |
44 |
45 |
46 | | {{ item.id }} |
47 | {{ item.name }} |
48 | {{ item.category }} |
49 | {{ item.language }} |
50 | {{ item.status }} |
51 |
52 |
53 |
54 |
55 | |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
83 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Template/Preview/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
![header]()
10 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
60 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Template/Update.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
20 |
21 |
22 |
37 |
--------------------------------------------------------------------------------
/stubs/interface/resources/js/components/Messenger/Ui/Breadcrumb.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
26 |
--------------------------------------------------------------------------------
/stubs/interface/resources/views/messenger.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Laravel Messenger
8 |
9 | @yield('title', config('app.name'))
10 | @vite(['resources/css/app.css', 'resources/js/app.js'])
11 |
12 |
13 | @yield('content')
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/stubs/interface/resources/views/template/create.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Laravel Messenger Templates Crear
8 |
9 | @yield('title', config('app.name'))
10 | @vite(['resources/css/app.css', 'resources/js/app.js'])
11 |
12 |
13 | @yield('content')
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/stubs/interface/resources/views/template/edit.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Laravel Messenger Template Actualizar
8 |
9 | @yield('title', config('app.name'))
10 | @vite(['resources/css/app.css', 'resources/js/app.js'])
11 |
12 |
13 | @yield('content')
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/stubs/interface/resources/views/template/index.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Laravel Messenger Templates
8 |
9 | @yield('title', config('app.name'))
10 | @vite(['resources/css/app.css', 'resources/js/app.js'])
11 |
12 |
13 | @yield('content')
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/stubs/interface/resources/views/template/show.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Laravel Messenger Template Detalles
8 |
9 | @yield('title', config('app.name'))
10 | @vite(['resources/css/app.css', 'resources/js/app.js'])
11 |
12 |
13 | @yield('content')
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/stubs/interface/tailwind.config.js:
--------------------------------------------------------------------------------
1 | import defaultTheme from 'tailwindcss/defaultTheme';
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | export default {
5 | content: [
6 | './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
7 | './storage/framework/views/*.php',
8 | './resources/views/**/*.blade.php',
9 | './resources/js/components/**/*.vue',
10 | ],
11 |
12 | theme: {
13 | extend: {
14 | fontFamily: {
15 | sans: ['Figtree', ...defaultTheme.fontFamily.sans],
16 | },
17 | },
18 | },
19 | daisyui: {
20 | themes: ['emerald', 'halloween']
21 | },
22 | plugins: [
23 | require('postcss-import'),
24 | require('tailwindcss'),
25 | require('autoprefixer'),
26 | require('daisyui'),
27 | ],
28 | };
29 |
--------------------------------------------------------------------------------
/stubs/interface/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import laravel from 'laravel-vite-plugin';
3 | import vue from '@vitejs/plugin-vue'
4 |
5 | export default defineConfig({
6 | plugins: [
7 | laravel({
8 | input: [
9 | 'resources/css/app.css',
10 | 'resources/js/app.js',
11 | ],
12 | refresh: true,
13 | }),
14 | vue(),
15 | ],
16 | resolve: {
17 | alias: {
18 | vue: 'vue/dist/vue.esm-bundler.js',
19 | },
20 | },
21 | });
22 |
--------------------------------------------------------------------------------
/tests/Fake/Message/FakeMessageCreteResponse.php:
--------------------------------------------------------------------------------
1 | 'whatsapp',
11 | 'contacts' => [
12 | [
13 | 'input' => '48XXXXXXXXX',
14 | 'wa_id' => '48XXXXXXXXX ',
15 | ],
16 | ],
17 | 'messages' => [
18 | [
19 | 'id' => $messageId,
20 | ],
21 | ],
22 | ];
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/Fake/Message/FakeMessageHook.php:
--------------------------------------------------------------------------------
1 | [
13 | [
14 | 'changes' => [
15 | [
16 | 'field' => 'messages',
17 | 'value' => [
18 | 'messaging_product' => 'whatsapp',
19 | 'id' => $message->message_id,
20 | 'metadata' => [
21 | 'phone_number_id' => $message->message_id,
22 | 'display_phone_number' => $message->chat->waba_phone,
23 | ],
24 | 'statuses' => [
25 | [
26 | 'id' => $message->message_id,
27 | 'status' => 'sent',
28 | 'timestamp' => '1709440196',
29 | 'recipient_id' => $message->chat->client_phone,
30 | 'conversation' => [
31 | 'id' => 'b2f9c8b2ebc0a5f66957383852b11a1e',
32 | 'expiration_timestamp' => '1709507460',
33 | 'origin' => ['type' => 'service'],
34 | ],
35 | ],
36 | ],
37 | ],
38 | ],
39 | ],
40 | ],
41 | ],
42 | ];
43 | }
44 |
45 | public static function getFakeMessageStatusWebhookDelivered(Message $message)
46 | {
47 | $request = self::getFakeMessageStatusWebhookSent($message);
48 | $request['entry'][0]['changes'][0]['value']['statuses'][0]['status'] = 'delivered';
49 |
50 | return $request;
51 | }
52 |
53 | public static function getFakeMessageStatusWebhookRead(Message $message)
54 | {
55 | $request = self::getFakeMessageStatusWebhookSent($message);
56 | $request['entry'][0]['changes'][0]['value']['statuses'][0]['status'] = 'read';
57 |
58 | return $request;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/tests/Fake/Template/FakeCreateTemplate.php:
--------------------------------------------------------------------------------
1 | $waba->id,
13 | 'name' => 'template_name',
14 | 'language' => 'en_US',
15 | 'category' => 'MARKETING',
16 | 'components' => [
17 | 'BODY' => [
18 | 'text' => 'Hello World',
19 | ],
20 | ],
21 | ];
22 | }
23 |
24 | public static function buttonsFooterBodyVarsTemplate($waba)
25 | {
26 | return [
27 | 'waba_id' => $waba->id,
28 | 'name' => 'template_name',
29 | 'language' => 'en_US',
30 | 'category' => 'MARKETING',
31 | 'components' => [
32 | 'BODY' => [
33 | 'text' => 'Hi {{1}}! For can get our {{2}} for as low as {{3}} for more information.',
34 | 'example' => [
35 | 'body_text' => [
36 | [
37 | 'Mark', 'Tuscan Getaway package', '800',
38 | ],
39 | ],
40 | ],
41 | ],
42 | 'FOOTER' => [
43 | 'text' => 'Shop now through to get of all merchandise.',
44 | ],
45 | 'BUTTONS' => [
46 | [
47 | 'type' => 'QUICK_REPLY',
48 | 'text' => 'Unsubcribe from Promos',
49 | ],
50 | [
51 | 'type' => 'PHONE_NUMBER',
52 | 'text' => 'Call',
53 | 'phone_number' => '15550051310',
54 | ],
55 | [
56 | 'type' => 'URL',
57 | 'text' => 'Shop Now',
58 | 'url' => 'https://www.examplesite.com/shop?promo={{1}}',
59 | 'example' => [
60 | 'summer2023',
61 | ],
62 | ],
63 |
64 | ],
65 | ],
66 | ];
67 | }
68 |
69 | public static function imageHeaderTemplate($waba)
70 | {
71 | $file = UploadedFile::fake()->create('file.jpg');
72 |
73 | return [
74 | 'waba_id' => $waba->id,
75 | 'name' => 'template_name',
76 | 'language' => 'en_US',
77 | 'category' => 'MARKETING',
78 | 'components' => [
79 | 'HEADER' => [
80 | 'format' => 'IMAGE',
81 | 'example' => [
82 | 'header_handle' => [$file],
83 | ],
84 | ],
85 | 'BODY' => [
86 | 'text' => 'Shop now through to get of all merchandise.',
87 | ],
88 | ],
89 | ];
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/tests/Fake/Template/FakeTemplateToMetaFormat.php:
--------------------------------------------------------------------------------
1 | $componentData) {
21 | $componentData['type'] = $index;
22 | $convertedComponents[] = $componentData;
23 | }
24 |
25 | return $convertedComponents;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/tests/Fake/Webhook/FakeReceivedMessage.php:
--------------------------------------------------------------------------------
1 | 'messages',
12 | 'value' => [
13 | 'messaging_product' => 'whatsapp',
14 | 'metadata' => [
15 | 'display_phone_number' => $wabaPhone->display_phone_number,
16 | 'phone_number_id' => $wabaPhone->phone_id,
17 | ],
18 | 'contacts' => [
19 | [
20 | 'profile' => [
21 | 'name' => 'Kerry Fisher',
22 | ],
23 | 'wa_id' => '16315551234',
24 | ],
25 | ],
26 | 'messages' => [
27 | [
28 | 'from' => '16315551234',
29 | 'id' => $messageId,
30 | 'timestamp' => '1603059201',
31 | 'text' => [
32 | 'body' => 'Hello this is an answer',
33 | ],
34 | 'type' => 'text',
35 | ],
36 | ],
37 | ],
38 | ]
39 | );
40 | }
41 |
42 | public static function responseTextMessage($wabaPhone, $messageId): array
43 | {
44 | return FakeWebhook::getBaseWebhook(
45 | [
46 | 'field' => 'messages',
47 | 'value' => [
48 | 'messaging_product' => 'whatsapp',
49 | 'metadata' => [
50 | 'display_phone_number' => $wabaPhone->display_phone_number,
51 | 'phone_number_id' => $wabaPhone->phone_id,
52 | ],
53 | 'contacts' => [
54 | [
55 | 'profile' => [
56 | 'name' => 'Kerry Fisher',
57 | ],
58 | 'wa_id' => '16315551234',
59 | ],
60 | ],
61 | 'messages' => [
62 | [
63 | 'context' => [
64 | 'from' => '16315558011',
65 | 'id' => $messageId.'-reply',
66 | ],
67 | 'from' => '16315551234',
68 | 'id' => $messageId,
69 | 'timestamp' => '1603059201',
70 | 'text' => [
71 | 'body' => 'Hello this is an answer',
72 | ],
73 | 'type' => 'text',
74 | ],
75 | ],
76 | ],
77 | ]
78 | );
79 | }
80 |
81 | public static function imageMessage($wabaPhone, $messageId): array
82 | {
83 | return FakeWebhook::getBaseWebhook(
84 | [
85 | 'field' => 'messages',
86 | 'value' => [
87 | 'messaging_product' => 'whatsapp',
88 | 'metadata' => [
89 | 'display_phone_number' => $wabaPhone->display_phone_number,
90 | 'phone_number_id' => $wabaPhone->phone_id,
91 | ],
92 | 'contacts' => [
93 | [
94 | 'profile' => [
95 | 'name' => 'Kerry Fisher',
96 | ],
97 | 'wa_id' => '16315551234',
98 | ],
99 | ],
100 | 'messages' => [
101 | [
102 | 'from' => '16315551234',
103 | 'id' => $messageId,
104 | 'timestamp' => '1603059201',
105 | 'type' => 'image',
106 | 'image' => [
107 | 'id' => $messageId.'xx',
108 | ],
109 | ],
110 | ],
111 | ],
112 | ]
113 | );
114 | }
115 |
116 | public static function reactionMessage($wabaPhone, $messageId): array
117 | {
118 | return FakeWebhook::getBaseWebhook(
119 | [
120 | 'field' => 'messages',
121 | 'value' => [
122 | 'messaging_product' => 'whatsapp',
123 | 'metadata' => [
124 | 'display_phone_number' => $wabaPhone->display_phone_number,
125 | 'phone_number_id' => $wabaPhone->phone_id,
126 | ],
127 | 'contacts' => [
128 | [
129 | 'profile' => [
130 | 'name' => 'Kerry Fisher',
131 | ],
132 | 'wa_id' => '16315551234',
133 | ],
134 | ],
135 | 'messages' => [
136 | [
137 | 'from' => '16315551234',
138 | 'id' => $messageId.'xx',
139 | 'timestamp' => '1603059201',
140 | 'type' => 'reaction',
141 | 'reaction' => [
142 | 'emoji' => 'Carita feliz',
143 | 'message_id' => $messageId,
144 | ],
145 | ],
146 | ],
147 | ],
148 | ]
149 | );
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/tests/Fake/Webhook/FakeTemplateStatusWebhook.php:
--------------------------------------------------------------------------------
1 | 'message_template_status_update',
12 | 'value' => [
13 | 'event' => $status,
14 | 'message_template_id' => $id,
15 | 'message_template_name' => $name,
16 | 'message_template_language' => 'es_MX',
17 | 'reason' => null,
18 | ],
19 | ]
20 | );
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/Fake/Webhook/FakeWebhook.php:
--------------------------------------------------------------------------------
1 | [
11 | [
12 | 'changes' => [
13 | $data,
14 | ],
15 | ],
16 | ],
17 | ];
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tests/Feature/Campaign/CreateCampaignTest.php:
--------------------------------------------------------------------------------
1 | faker->numberBetween(111, 450);
19 | $campaign = Campaign::factory()->make();
20 | $phones = [
21 | $this->faker->e164PhoneNumber(),
22 | $this->faker->e164PhoneNumber(),
23 | $this->faker->e164PhoneNumber(),
24 | ];
25 | $file = UploadedFile::fake()->createWithContent('document.csv', implode(',', $phones));
26 |
27 | Http::fake([
28 | '*/messages' => Http::response(FakeMessageCreteResponse::getFakeMessageCreateResponse($messageId)),
29 | ]);
30 |
31 | $this->post(route('campaign.store'), [
32 | 'name' => $campaign->name,
33 | 'waba_phone_id' => $campaign->waba_phone_id,
34 | 'template_id' => $campaign->template_id,
35 | 'file' => $file,
36 | ])->assertStatus(200);
37 |
38 | $this->assertDatabaseHas('campaigns', [
39 | 'name' => $campaign->name,
40 | 'waba_phone_id' => $campaign->waba_phone_id,
41 | 'template_id' => $campaign->template_id,
42 | 'total_messages' => count($phones),
43 | ]);
44 |
45 | foreach ($phones as $phone) {
46 | $this->assertDatabaseHas('chats', [
47 | 'client_phone' => $phone,
48 | ]);
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/tests/Feature/Chat/ChatTest.php:
--------------------------------------------------------------------------------
1 | count(10)->create();
13 |
14 | $this->get(route('chat.index'))
15 | ->assertJsonCount(10, 'data')
16 | ->assertStatus(200);
17 | }
18 |
19 | public function test_get_chat_filter_by_client_phone()
20 | {
21 | Chat::factory()->count(10)->create();
22 | $chat = Chat::factory()->create();
23 |
24 | $this->get(route('chat.index').'?client_phone='.$chat->client_phone)
25 | ->assertJsonCount(1, 'data')
26 | ->assertStatus(200);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tests/Feature/Commands/CopyMessengerCommandTest.php:
--------------------------------------------------------------------------------
1 | partialMock(Filesystem::class, function (MockInterface $mock) {
18 | $mock->shouldReceive('deleteDirectory')->once();
19 | });
20 |
21 | $this->artisan('sdk:copy-messenger-to-package')
22 | ->expectsConfirmation('Esto sobrescribira el messenger del paquete usando el messenger del proyecto principal, estas seguro de continuar?', 'yes')
23 | ->expectsOutput('Copiando messenger a los stubs del packate...')
24 | ->expectsOutput('SDK Whatsapp Messenger se copio correctamente.')
25 | ->assertExitCode(0);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/tests/Feature/Commands/InstallTest.php:
--------------------------------------------------------------------------------
1 | partialMock(FileManager::class, function (MockInterface $mock) {
17 | $mock->shouldReceive('replace')->once();
18 | });
19 |
20 | $this->artisan('sdk:whatsapp-messenger-install')
21 | ->expectsConfirmation('¿Deseas instalar la interfaz, grafica?', 'yes')
22 | ->expectsOutput('Instalando interfaz...')
23 | ->expectsOutput('Actualizando Node Packages...')
24 | ->expectsOutput('Copiando archivos de configuración de la interface...')
25 | ->expectsOutput('Instalando librería...')
26 | ->expectsOutput('Habilitando BroadcastServiceProvider...')
27 | ->expectsOutput('Copiando archivos de configuración de la libreria...')
28 | ->expectsOutput('Copiando archivos de configuración para laravel 11...')
29 | ->expectsOutput('SDK Whatsapp Messenger instalado correctamente.')
30 | ->expectsOutput('Ejecuta "npm install && npm run dev" para compilar tu frontend.')
31 | ->assertExitCode(0);
32 | }
33 |
34 | public function test_install_without_interface()
35 | {
36 | $this->partialMock(FileManager::class, function (MockInterface $mock) {
37 | $mock->shouldReceive('replace')->once();
38 | });
39 |
40 | $this->artisan('sdk:whatsapp-messenger-install')
41 | ->expectsConfirmation('¿Deseas instalar la interfaz, grafica?', 'no')
42 | ->doesntExpectOutput('Instalando interfaz...')
43 | ->doesntExpectOutput('Actualizando Node Packages...')
44 | ->doesntExpectOutput('Copiando archivos de configuración de la interface...')
45 | ->expectsOutput('Instalando librería...')
46 | ->expectsOutput('Habilitando BroadcastServiceProvider...')
47 | ->expectsOutput('Copiando archivos de configuración de la libreria...')
48 | ->expectsOutput('Copiando archivos de configuración para laravel 11...')
49 | ->expectsOutput('SDK Whatsapp Messenger instalado correctamente.')
50 | ->expectsOutput('Ejecuta "npm install && npm run dev" para compilar tu frontend.')
51 | ->assertExitCode(0);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tests/Feature/Message/MessageStatusWebhookTest.php:
--------------------------------------------------------------------------------
1 | create();
14 | $this->post(route('meta.webhook'), FakeMessageRequests::getFakeMessageStatusWebhookSent($message));
15 |
16 | $this->assertDatabaseHas('messages', [
17 | 'message_id' => $message->message_id,
18 | 'sent_at' => '1709440196',
19 | ]);
20 |
21 | $this->assertDatabaseHas('chats', [
22 | 'waba_phone' => $message->chat->waba_phone,
23 | 'expiration_timestamp' => '1709507460',
24 | ]);
25 | }
26 |
27 | public function test_delivered_hook()
28 | {
29 | $message = Message::factory()->create();
30 | $this->post(route('meta.webhook'), FakeMessageRequests::getFakeMessageStatusWebhookDelivered($message));
31 |
32 | $this->assertDatabaseHas('messages', [
33 | 'message_id' => $message->message_id,
34 | 'delivered_at' => '1709440196',
35 | ]);
36 | }
37 |
38 | public function test_read_hook()
39 | {
40 | $message = Message::factory()->create();
41 | $this->post(route('meta.webhook'), FakeMessageRequests::getFakeMessageStatusWebhookRead($message));
42 |
43 | $this->assertDatabaseHas('messages', [
44 | 'message_id' => $message->message_id,
45 | 'read_at' => '1709440196',
46 | ]);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tests/Feature/Message/MessageTest.php:
--------------------------------------------------------------------------------
1 | create();
17 | Message::factory()->count(5)->create();
18 | Message::factory()->count(6)->create(['chat_id' => $chat->id]);
19 | Message::factory()->create([
20 | 'chat_id' => $chat->id,
21 | 'type' => 'image',
22 | 'body' => '{"image":{"url":"https:\/\/los-chavos.site\/storage\/received\/16\/2514991862013041.jpg"}}',
23 | ]);
24 | Message::factory()->create([
25 | 'chat_id' => $chat->id,
26 | 'type' => 'contacts',
27 | 'body' => '{"contacts":{"contact":"Mire esta slegible"}}',
28 | ]);
29 | Message::factory()->create([
30 | 'chat_id' => $chat->id,
31 | 'type' => 'contacts',
32 | 'body' => '{"contacts":{"contact":"Mire esta slegible"},"context": "contexto"}',
33 | ]);
34 | Message::factory()->create([
35 | 'chat_id' => $chat->id,
36 | 'type' => 'template',
37 | 'body' => '{}',
38 | ]);
39 |
40 | $this->get(route('message.index').'?chat_id='.$chat->id)
41 | ->assertJsonCount(10, 'data')
42 | ->assertStatus(200);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/Feature/Message/SendMessagesTest.php:
--------------------------------------------------------------------------------
1 | faker()->numberBetween(111, 450);
20 | $wabaPhone = WabaPhone::factory()->create();
21 |
22 | Http::fake([
23 | "*/$wabaPhone->phone_id/messages" => Http::response(FakeMessageCreteResponse::getFakeMessageCreateResponse($messageId)),
24 | ]);
25 |
26 | $this->post(route('message.send'), [
27 | 'waba_phone_id' => $wabaPhone->id,
28 | 'to' => '2213428198',
29 | 'message' => [
30 | 'type' => 'text',
31 | 'text' => [
32 | 'preview_url' => false,
33 | 'body' => 'text-message-content',
34 | ],
35 | ],
36 | ])
37 | ->assertStatus(200);
38 |
39 | $this->assertDatabaseHas('messages', [
40 | 'direction' => 'toClient',
41 | 'message_id' => $messageId,
42 | ]);
43 | }
44 |
45 | public function test_send_image_message()
46 | {
47 | $messageId = 'wamid.'.$this->faker()->numberBetween(111, 450);
48 | $wabaPhone = WabaPhone::factory()->create();
49 |
50 | Http::fake([
51 | "*/$wabaPhone->phone_id/media" => Http::response(['id' => $this->faker()->uuid]),
52 | "*/$wabaPhone->phone_id/messages" => Http::response(FakeMessageCreteResponse::getFakeMessageCreateResponse($messageId)),
53 | ]);
54 |
55 | Storage::fake('local');
56 | $file = UploadedFile::fake()->image('avatar.jpg', 100, 100)->size(100);
57 |
58 | $this->post(route('message.send'), [
59 | 'waba_phone_id' => $wabaPhone->id,
60 | 'to' => '2213428198',
61 | 'message' => [
62 | 'type' => 'image',
63 | 'image' => $file,
64 | ],
65 | ])
66 | ->assertStatus(200);
67 |
68 | $this->assertDatabaseHas('messages', [
69 | 'direction' => 'toClient',
70 | 'message_id' => $messageId,
71 | ]);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/tests/Feature/Message/WabaPhoneTest.php:
--------------------------------------------------------------------------------
1 | count(10)->create();
21 |
22 | $this->get(route('waba_phone.waba_number'))
23 | ->assertJsonCount(10, 'data')
24 | ->assertStatus(200);
25 | }
26 |
27 | public function test_get_all_waba_numbers_filter_by_waba()
28 | {
29 | $waba = Waba::factory()->create();
30 | WabaPhone::factory()->count(10)->create();
31 | WabaPhone::factory(['waba_id' => $waba->id])->count(5)->create();
32 | WabaPhone::factory()->count(10)->create();
33 |
34 | $this->get(route('waba_phone.waba_number')."?waba_id=$waba->id")
35 | ->assertJsonCount(5, 'data')
36 | ->assertStatus(200);
37 | }
38 |
39 | public function test_get_bussines_profile()
40 | {
41 | $wabaPhone = WabaPhone::factory()->create();
42 | Http::fake([
43 | "*/$wabaPhone->phone_id/whatsapp_business_profile?fields=about,address,description,email,profile_picture_url,websites,vertical" => Http::response(FakeWabaResponses::fakeBussinesProfile(), 200),
44 | ]);
45 |
46 | $this->get(route('waba_phone.bussines_profile', ['phoneId' => $wabaPhone->phone_id]))->assertStatus(200);
47 | }
48 |
49 | public function test_set_bussines_profile()
50 | {
51 | $wabaPhone = WabaPhone::factory()->create();
52 | $sessionId = $this->faker()->uuid;
53 | Storage::fake('local');
54 | $file = UploadedFile::fake()->create('file.jpg');
55 |
56 | Http::fake([
57 | '*/uploads*' => Http::response(['id' => $sessionId]),
58 | "*/$sessionId" => Http::response(['h' => $this->faker()->uuid]),
59 | "*/$wabaPhone->phone_id/whatsapp_business_profile" => Http::response(['success' => true], 200),
60 | "*/$wabaPhone->phone_id/whatsapp_business_profile?fields=about,address,description,email,profile_picture_url,websites,vertical" => Http::response(FakeWabaResponses::fakeBussinesProfile(), 200),
61 | ]);
62 |
63 | $this->post(route('waba_phone.storage_bussines_profile', ['phoneId' => $wabaPhone->phone_id]), [
64 | 'address' => $wabaPhone['address'],
65 | 'description' => $wabaPhone['description'],
66 | 'vertical' => $wabaPhone['vertical'],
67 | 'about' => $wabaPhone['about'],
68 | 'email' => $wabaPhone['email'],
69 | 'websites' => json_decode($wabaPhone['websites']),
70 | 'picture_profile' => $file,
71 | ])->assertStatus(200);
72 | }
73 |
74 | public function test_set_bussines_profile_without_profile_picture()
75 | {
76 | $wabaPhone = WabaPhone::factory()->create();
77 | $sessionId = $this->faker()->uuid;
78 |
79 | Http::fake([
80 | '*/uploads*' => Http::response(['id' => $sessionId]),
81 | "*/$sessionId" => Http::response(['h' => $this->faker()->uuid]),
82 | "*/$wabaPhone->phone_id/whatsapp_business_profile" => Http::response(['success' => true], 200),
83 | "*/$wabaPhone->phone_id/whatsapp_business_profile?fields=about,address,description,email,profile_picture_url,websites,vertical" => Http::response(FakeWabaResponses::fakeBussinesProfile(), 200),
84 | ]);
85 |
86 | $this->post(route('waba_phone.storage_bussines_profile', ['phoneId' => $wabaPhone->phone_id]), [
87 | 'address' => $wabaPhone['address'],
88 | 'description' => $wabaPhone['description'],
89 | 'vertical' => $wabaPhone['vertical'],
90 | 'about' => $wabaPhone['about'],
91 | 'email' => $wabaPhone['email'],
92 | 'websites' => json_decode($wabaPhone['websites']),
93 | ])->assertStatus(200);
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/tests/Feature/Services/FileManagerTest.php:
--------------------------------------------------------------------------------
1 | unique()->word();
16 | FileManager::create($file_path);
17 | $this->assertTrue(file_exists($file_path));
18 |
19 | return $file_path;
20 | }
21 |
22 | #[Depends('test_create_file')]
23 | public function test_append_to_file(string $file_path): array
24 | {
25 | $faker = Factory::create();
26 | $word = $faker->unique()->name();
27 |
28 | FileManager::append($file_path, $word);
29 |
30 | $file_content = file_get_contents($file_path);
31 |
32 | $this->assertStringContainsString($word, $file_content);
33 |
34 | return [
35 | 'word' => $word,
36 | 'file_path' => $file_path,
37 | ];
38 | }
39 |
40 | #[Depends('test_append_to_file')]
41 | public function test_writte_after_to_file(array $data): void
42 | {
43 | $faker = Factory::create();
44 | $new_word = $faker->unique()->name();
45 |
46 | FileManager::writteAfter($data['word'], $new_word, $data['file_path']);
47 |
48 | $file_content = file_get_contents($data['file_path']);
49 |
50 | // $this->assertStringNotContainsString($data['word'], $file_content);
51 | $this->assertStringContainsString($data['word'].$new_word, $file_content);
52 | }
53 |
54 | #[Depends('test_append_to_file')]
55 | public function test_replace_file(array $data): void
56 | {
57 | $faker = Factory::create();
58 | $new_word = $faker->unique()->name();
59 |
60 | FileManager::replace($data['word'], $new_word, $data['file_path']);
61 |
62 | $file_content = file_get_contents($data['file_path']);
63 |
64 | $this->assertStringNotContainsString($data['word'], $file_content);
65 | $this->assertStringContainsString($new_word, $file_content);
66 | }
67 |
68 | public function test_append_json_file(): void
69 | {
70 | $new_content = ['content' => 'new-content'];
71 |
72 | $file_path = $this->createJsonFile();
73 | FileManager::appendToJsonKey($file_path, $new_content, 'devDependencies');
74 |
75 | $new_json = $this->getJsonContent();
76 | $new_json['devDependencies'] = array_merge($new_json['devDependencies'], $new_content);
77 |
78 | $this->assertJsonStringEqualsJsonFile(
79 | $file_path,
80 | json_encode($new_json)
81 | );
82 | }
83 |
84 | protected function createJsonFile(): string
85 | {
86 | $faker = Factory::create();
87 | $file_path = __DIR__.'/files/'.$faker->unique()->word().'.json';
88 | FileManager::create($file_path);
89 | FileManager::writteJson($file_path, $this->getJsonContent());
90 |
91 | return $file_path;
92 | }
93 |
94 | protected function getJsonContent()
95 | {
96 | return [
97 | 'scripts' => [
98 | 'dev' => 'npm run development',
99 | 'development' => 'mix',
100 | 'watch' => 'mix watch',
101 | ],
102 | 'devDependencies' => [
103 | 'axios' => '^0.25',
104 | 'laravel-mix' => '^6.0.6',
105 | 'lodash' => '^4.17.19',
106 | 'postcss' => '^8.1.14',
107 | ],
108 | ];
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/tests/Feature/Services/MediaManagerServiceTest.php:
--------------------------------------------------------------------------------
1 | fakeDownloadFile();
15 | Storage::fake('local');
16 |
17 | $service = resolve(MediaManagerService::class);
18 | $service->download('7096261007159140', '104246142661561', 'archivo');
19 |
20 | Storage::disk('local')->assertExists('archivo.jpg');
21 | }
22 |
23 | private function fakeDownloadFile()
24 | {
25 | Http::fake([
26 | 'https://graph.facebook.com/*' => Http::response([
27 | 'url' => 'https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=7096',
28 | 'mime_type' => 'image/jpeg',
29 | 'sha256' => 'b2888367f26694b854d12e1c7895f5402944b70b121370014a610822ea318eb2',
30 | 'file_size' => 21619,
31 | 'id' => '7096261007159140',
32 | 'messaging_product' => 'whatsapp',
33 | ], 200),
34 | 'https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=7096' => Http::response('file', 200, [
35 | 'Content-Disposition' => 'inline;filename=File.jpg',
36 | ]),
37 | ]);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tests/Feature/Services/files/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
--------------------------------------------------------------------------------
/tests/Feature/Template/CreateTemplateTest.php:
--------------------------------------------------------------------------------
1 | create();
20 | $templateId = $this->faker()->uuid;
21 | $templateRequest = FakeCreateTemplate::textTemplate($waba);
22 | $templateMetaFormat = FakeTemplateToMetaFormat::convert($templateRequest, $templateId);
23 |
24 | Http::fake([
25 | "*/$waba->waba_id/message_templates" => Http::response([
26 | 'id' => $templateId,
27 | 'status' => 'PENDING',
28 | 'category' => 'MARKETING',
29 | ]),
30 | "*/$templateId" => Http::response($templateMetaFormat),
31 | ]);
32 |
33 | $this->post(route('template.store'), $templateRequest)->assertStatus(201);
34 | $this->assertDatabase($waba);
35 | }
36 |
37 | public function test_create_template_with_buttons_footer_buttons()
38 | {
39 | $waba = Waba::factory()->create();
40 | $templateId = $this->faker()->uuid;
41 | $templateRequest = FakeCreateTemplate::buttonsFooterBodyVarsTemplate($waba);
42 | $templateMetaFormat = FakeTemplateToMetaFormat::convert($templateRequest, $templateId);
43 |
44 | Http::fake([
45 | "*/$waba->waba_id/message_templates" => Http::response([
46 | 'id' => $templateId,
47 | 'status' => 'PENDING',
48 | 'category' => 'MARKETING',
49 | ]),
50 | "*/$templateId" => Http::response($templateMetaFormat),
51 | ]);
52 |
53 | $this->post(route('template.store'), $templateRequest)->assertStatus(201);
54 | $this->assertDatabase($waba);
55 | }
56 |
57 | public function test_create_template_with_image_header()
58 | {
59 | $sessionId = $this->faker()->uuid;
60 | $waba = Waba::factory()->create();
61 | $templateId = $this->faker()->uuid;
62 | $templateRequest = FakeCreateTemplate::imageHeaderTemplate($waba);
63 | $templateMetaFormat = FakeTemplateToMetaFormat::convert($templateRequest, $templateId);
64 | $templateMetaFormat['components'][0]['example']['header_handle'] = ['4::aW1hZ2U6Ly9pb'];
65 |
66 | Http::fake([
67 | "*/$templateId" => Http::response($templateMetaFormat),
68 | '*/uploads*' => Http::response(['id' => $sessionId]),
69 | "*/$sessionId" => Http::response(['h' => $this->faker()->uuid]),
70 | "*/$waba->waba_id/message_templates" => Http::response([
71 | 'id' => $templateId,
72 | 'status' => 'PENDING',
73 | 'category' => 'MARKETING',
74 | ]),
75 | ]);
76 |
77 | Storage::fake('local');
78 | $this->post(route('template.store'), $templateRequest)->assertStatus(201);
79 | $this->assertDatabase($waba);
80 | }
81 |
82 | private function assertDatabase(Waba $waba)
83 | {
84 | return $this->assertDatabaseHas('templates', [
85 | 'waba_id' => $waba->id,
86 | 'name' => 'template_name',
87 | 'language' => 'en_US',
88 | 'category' => 'MARKETING',
89 | 'status' => 'PENDING',
90 | ]);
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/tests/Feature/Template/TemplateStatusWebhookTest.php:
--------------------------------------------------------------------------------
1 | 'PENDING',
18 | ])->create();
19 |
20 | $webhookRequest = FakeTemplateStatusWebhook::statusWebHookRequest($template->template_id, $template->name, 'APPROVED');
21 | $this->post(route('meta.webhook'), $webhookRequest)->assertStatus(200);
22 |
23 | $this->assertDatabaseHas('templates', [
24 | 'id' => $template->id,
25 | 'status' => 'APPROVED',
26 | ]);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tests/Feature/Template/TemplateTest.php:
--------------------------------------------------------------------------------
1 | count(10)->create();
13 |
14 | $this->get(route('template.index'))
15 | ->assertJsonCount(10, 'data')
16 | ->assertStatus(200);
17 | }
18 |
19 | public function test_get_all_templates_filter_by_approved()
20 | {
21 | Template::factory()->count(1)->create();
22 | Template::factory(['status' => 'PENDING'])->count(10)->create();
23 |
24 | $this->get(route('template.index').'?status='.Template::STATUS_APPROVED)
25 | ->assertJsonCount(1, 'data')
26 | ->assertStatus(200);
27 | }
28 |
29 | public function test_get_all_templates_filter_by_name()
30 | {
31 | Template::factory()->count(1)->create();
32 | Template::factory(['status' => 'PENDING'])->count(3)->create();
33 | $template = Template::factory()->create();
34 |
35 | $this->get(route('template.index').'?name='.$template->name)
36 | ->assertJsonCount(1, 'data')
37 | ->assertStatus(200);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tests/Feature/Waba/WabaManagerTest.php:
--------------------------------------------------------------------------------
1 | Http::response($fakeTemplates, 200),
20 | ]);
21 |
22 | $waba = Waba::factory()->create(['waba_id' => '121544050937574']);
23 | $this->get(route('waba.getTemplatesFromMeta', ['wabaId' => $waba->waba_id]))->assertStatus(200);
24 |
25 | foreach ($fakeTemplates['data'] as $fakeTemplate) {
26 | $this->assertDatabaseHas('templates', [
27 | 'waba_id' => $waba->waba_id,
28 | 'name' => $fakeTemplate['name'],
29 | ]);
30 | }
31 |
32 | }
33 |
34 | public function test_get_templates_from_waba_template_caroucel()
35 | {
36 | $fakeTemplates = FakeWabaResponses::fakeTemplateCaroucel();
37 | Http::fake([
38 | '*/message_templates' => Http::response($fakeTemplates, 200),
39 | ]);
40 |
41 | $waba = Waba::factory()->create(['waba_id' => '121544050937574']);
42 | $this->get(route('waba.getTemplatesFromMeta', ['wabaId' => $waba->waba_id]))->assertStatus(200);
43 |
44 | foreach ($fakeTemplates['data'] as $fakeTemplate) {
45 | $this->assertDatabaseHas('templates', [
46 | 'waba_id' => $waba->waba_id,
47 | 'name' => $fakeTemplate['name'],
48 | ]);
49 | }
50 |
51 | $this->assertDatabaseCount('templates', 1);
52 | }
53 |
54 | public function test_get_waba_info()
55 | {
56 | $wabaId = '104996122399160';
57 | $wabaFakeInfo = FakeWabaResponses::getFakeWabaInfo();
58 |
59 | Http::fake(["*$wabaId" => Http::response($wabaFakeInfo, 200)]);
60 |
61 | $this->get(route('waba.getInfoFromMeta', ['wabaId' => $wabaId]))->assertStatus(200);
62 |
63 | $this->assertDatabaseHas('wabas', [
64 | 'waba_id' => $wabaFakeInfo['id'],
65 | 'name' => $wabaFakeInfo['name'],
66 | 'timezone_id' => $wabaFakeInfo['timezone_id'],
67 | 'currency' => $wabaFakeInfo['currency'],
68 | 'message_template_namespace' => $wabaFakeInfo['message_template_namespace'],
69 | ]);
70 | }
71 |
72 | public function test_get_phone_numbers_from_waba()
73 | {
74 | $waba = Waba::factory()->create();
75 | $wabaId = $waba->waba_id;
76 | $wabaPhonesFake = FakeWabaResponses::fakePhoneNumbers();
77 |
78 | Http::fake([
79 | "*$wabaId/phone_numbers" => Http::response($wabaPhonesFake, 200),
80 | '*/whatsapp_business_profile?fields=about,address,description,email,profile_picture_url,websites,vertical' => Http::response(FakeWabaResponses::fakeBussinesProfile(), 200),
81 | ]);
82 |
83 | $this->get(route('waba.getPhonesFromMeta', ['wabaId' => $wabaId]))->assertStatus(200);
84 |
85 | foreach ($wabaPhonesFake['data'] as $wabaPhoneFake) {
86 | $this->assertDatabaseHas('waba_phones', [
87 | 'name' => $wabaPhoneFake['verified_name'],
88 | 'display_phone_number' => $wabaPhoneFake['display_phone_number'],
89 | 'phone_id' => $wabaPhoneFake['id'],
90 | 'quality_rating' => $wabaPhoneFake['quality_rating'],
91 | ]);
92 | }
93 | }
94 |
95 | public function test_waba_init()
96 | {
97 | $waba = Waba::factory()->create();
98 | $wabaId = $waba->waba_id;
99 |
100 | Http::fake([
101 | "*$wabaId" => Http::response(FakeWabaResponses::getFakeWabaInfo(), 200),
102 | "*$wabaId/phone_numbers" => Http::response(FakeWabaResponses::fakePhoneNumbers(), 200),
103 | '*/message_templates' => Http::response(FakeWabaResponses::fakeTemplates(), 200),
104 | '*/whatsapp_business_profile?fields=about,address,description,email,profile_picture_url,websites,vertical' => Http::response(FakeWabaResponses::fakeBussinesProfile(), 200),
105 | ]);
106 |
107 | $this->get(route('waba.init', ['wabaId' => $wabaId]))->assertStatus(200);
108 | }
109 |
110 | public function test_get_phone_numbers_from_waba_no_profile()
111 | {
112 | $waba = Waba::factory()->create();
113 | $wabaId = $waba->waba_id;
114 | $wabaPhonesFake = FakeWabaResponses::fakePhoneNumbers();
115 |
116 | Http::fake([
117 | "*$wabaId/phone_numbers" => Http::response($wabaPhonesFake, 200),
118 | ]);
119 |
120 | $this->get(route('waba.getPhonesFromMeta', ['wabaId' => $wabaId]))->assertStatus(200);
121 |
122 | foreach ($wabaPhonesFake['data'] as $wabaPhoneFake) {
123 | $this->assertDatabaseHas('waba_phones', [
124 | 'name' => $wabaPhoneFake['verified_name'],
125 | 'display_phone_number' => $wabaPhoneFake['display_phone_number'],
126 | 'phone_id' => $wabaPhoneFake['id'],
127 | 'quality_rating' => $wabaPhoneFake['quality_rating'],
128 | ]);
129 | }
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/tests/Feature/Waba/WabaTest.php:
--------------------------------------------------------------------------------
1 | create();
16 |
17 | $this->get(route('waba.index'))
18 | ->assertStatus(200)
19 | ->assertJsonCount(1, 'data');
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/Feature/Waba/WebhookSubscribeTest.php:
--------------------------------------------------------------------------------
1 | get(route('meta.webhook.subscribe').'?hub_verify_token=23653244');
12 |
13 | $response->assertStatus(200);
14 | }
15 |
16 | public function test_subscribe_webhook_with_invalid_token()
17 | {
18 | $response = $this->get(route('meta.webhook.subscribe'));
19 |
20 | $response->assertStatus(400);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | loadLaravelMigrations();
22 | $this->artisan('migrate')->run();
23 |
24 | self::$customMigration = true;
25 | }
26 | }
27 |
28 | protected function getEnvironmentSetUp($app)
29 | {
30 | $app['config']->set('database.default', 'testbench');
31 | $app['config']->set('database.connections.testbench', [
32 | 'driver' => 'sqlite',
33 | 'database' => ':memory:',
34 | 'prefix' => '',
35 | ]);
36 |
37 | $app['config']->set('logging.default', 'single');
38 | $app['config']->set('logging.channels.single', [
39 | 'driver' => 'single',
40 | 'path' => __DIR__.'/logs/test.log',
41 | 'level' => 'debug',
42 | ]);
43 | }
44 |
45 | /**
46 | * @param \Illuminate\Foundation\Application $app
47 | * @return array
48 | */
49 | protected function getPackageProviders($app)
50 | {
51 | return [
52 | ServiceProvider::class,
53 | OpenAiServiceProvider::class,
54 | ];
55 | }
56 |
57 | protected function getPackageAliases($app)
58 | {
59 | return [
60 | 'WhatsappCloudApi' => WhatsappCloudApi::class,
61 | ];
62 | }
63 |
64 | /**
65 | * Ignore package discovery from.
66 | *
67 | * @return array
68 | */
69 | public function ignorePackageDiscoveriesFrom()
70 | {
71 | return [];
72 | }
73 | }
74 |
--------------------------------------------------------------------------------