diff --git a/.env.example b/.env.example index 7493df5b5..4f6e2bddc 100644 --- a/.env.example +++ b/.env.example @@ -134,4 +134,5 @@ TEST_ZOHO_CLIENT_REFRESH_TOKEN= TEST_SHOPIFY_API_KEY= TEST_SHOPIFY_API_SECRET= -TEST_SHOPIFY_SHOP_URL= \ No newline at end of file +TEST_SHOPIFY_SHOP_URL= +TEST_STRIPE_SECRET_KEY= diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 73797611a..7e80caf17 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -58,6 +58,8 @@ jobs: TEST_SHOPIFY_SHOP_URL: ${{ secrets.TEST_SHOPIFY_SHOP_URL }} TEST_APPLE_LOGIN_TOKEN: ${{ secrets.TEST_APPLE_LOGIN_TOKEN }} TEST_APOLLO_KEY: ${{ secrets.TEST_APOLLO_KEY }} + TEST_STRIPE_SECRET_KEY: ${{ secrets.TEST_STRIPE_SECRET_KEY }} + strategy: fail-fast: false matrix: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 381222f0e..9f2e82ff9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -66,6 +66,7 @@ jobs: TEST_SHOPIFY_SHOP_URL: ${{ secrets.TEST_SHOPIFY_SHOP_URL }} TEST_APPLE_LOGIN_TOKEN: ${{ secrets.TEST_APPLE_LOGIN_TOKEN }} TEST_APOLLO_KEY: ${{ secrets.TEST_APOLLO_KEY }} + TEST_STRIPE_SECRET_KEY: ${{ secrets.TEST_STRIPE_SECRET_KEY }} strategy: fail-fast: false matrix: diff --git a/app/Console/Commands/CreateEntityWorkflowCommand.php b/app/Console/Commands/Workflows/CreateEntityWorkflowCommand.php similarity index 99% rename from app/Console/Commands/CreateEntityWorkflowCommand.php rename to app/Console/Commands/Workflows/CreateEntityWorkflowCommand.php index d7caa7740..bebd584c2 100644 --- a/app/Console/Commands/CreateEntityWorkflowCommand.php +++ b/app/Console/Commands/Workflows/CreateEntityWorkflowCommand.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Console\Commands; +namespace App\Console\Commands\Workflows; use Illuminate\Console\Command; use Kanvas\Apps\Models\Apps; diff --git a/app/Console/Commands/Workflows/KanvasCreateReceiverCommand.php b/app/Console/Commands/Workflows/KanvasCreateReceiverCommand.php new file mode 100644 index 000000000..0a02df291 --- /dev/null +++ b/app/Console/Commands/Workflows/KanvasCreateReceiverCommand.php @@ -0,0 +1,62 @@ +info('Creating Receiver...'); + $app = select( + label: 'Select the app for the receiver: ', + options: Apps::pluck('name', 'id'), + ); + + $action = select( + label: 'Select the action for the receiver: ', + options: WorkflowAction::pluck('name', 'id'), + ); + + $userId = $this->ask('Enter the user ID for the receiver: '); + $companyId = $this->ask('Enter the company ID for the receiver: '); + $name = $this->ask('Enter the name for the receiver: '); + $description = $this->ask('Enter the description for the receiver: '); + + $company = Companies::getById($companyId); + + $user = UsersRepository::getUserOfCompanyById($company, (int)$userId); + + $receiver = ReceiverWebhook::create([ + 'apps_id' => $app, + 'action_id' => $action, + 'companies_id' => $company->getId(), + 'users_id' => $user->getId(), + 'name' => $name, + 'description' => $description, + 'is_active' => true, + 'is_deleted' => false, + ]); + + $this->info('Receiver created successfully!'); + $url = config('app.url') . '/receiver/' . $receiver->uuid; + $this->info('Webhook URL: ' . $url); + } +} diff --git a/app/Http/Controllers/ReceiverController.php b/app/Http/Controllers/ReceiverController.php index 0f1537c8c..c5640a51b 100644 --- a/app/Http/Controllers/ReceiverController.php +++ b/app/Http/Controllers/ReceiverController.php @@ -10,6 +10,7 @@ use Illuminate\Routing\Controller as BaseController; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Log; use Kanvas\Apps\Models\Apps; use Kanvas\Connectors\Zoho\Actions\SyncZohoAgentAction; use Kanvas\Connectors\Zoho\Actions\SyncZohoLeadAction; @@ -30,7 +31,6 @@ public function store(string $uuid, Request $request): JsonResponse { $app = app(Apps::class); $receiver = ReceiverWebhook::where('uuid', $uuid)->notDeleted()->first(); - if ($receiver) { // return response()->json(['message' => 'Receiver not found'], 404); if ($app->getId() != $receiver->apps_id) { diff --git a/composer.json b/composer.json index 9ffe4913c..ffe5a67be 100644 --- a/composer.json +++ b/composer.json @@ -46,6 +46,7 @@ "spatie/laravel-health": "^1.27", "spatie/laravel-queueable-action": "^2.15", "spatie/laravel-webhook-server": "^3.8", + "stripe/stripe-php": "^15.0", "symfony/expression-language": "^7.0", "symfony/http-client": "^7.0", "symfony/mailgun-mailer": "^7.0", diff --git a/composer.lock b/composer.lock index 9aea50c7a..fa72f837d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "66a75e57f8f8feba0b4e9acf4ac16f7c", + "content-hash": "9fe1b13ba14b05cd9bf48c1443242686", "packages": [ { "name": "algolia/algoliasearch-client-php", @@ -1195,16 +1195,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.315.5", + "version": "3.316.2", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "3e6d619d45d8e1a8681dd58de61ddfe90e8341e6" + "reference": "4d8caae512c3be4d59ee6d583b3f82872dde5071" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/3e6d619d45d8e1a8681dd58de61ddfe90e8341e6", - "reference": "3e6d619d45d8e1a8681dd58de61ddfe90e8341e6", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/4d8caae512c3be4d59ee6d583b3f82872dde5071", + "reference": "4d8caae512c3be4d59ee6d583b3f82872dde5071", "shasum": "" }, "require": { @@ -1284,9 +1284,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.315.5" + "source": "https://github.com/aws/aws-sdk-php/tree/3.316.2" }, - "time": "2024-07-03T18:12:51+00:00" + "time": "2024-07-10T19:16:28+00:00" }, { "name": "berkayk/onesignal-laravel", @@ -2673,16 +2673,16 @@ }, { "name": "google/cloud-storage", - "version": "v1.42.0", + "version": "v1.42.1", "source": { "type": "git", "url": "https://github.com/googleapis/google-cloud-php-storage.git", - "reference": "1c77f5882c30bec95ab2837b9534a946325d1c57" + "reference": "2a418cad887e44d08a86de19a878ea3607212edb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleapis/google-cloud-php-storage/zipball/1c77f5882c30bec95ab2837b9534a946325d1c57", - "reference": "1c77f5882c30bec95ab2837b9534a946325d1c57", + "url": "https://api.github.com/repos/googleapis/google-cloud-php-storage/zipball/2a418cad887e44d08a86de19a878ea3607212edb", + "reference": "2a418cad887e44d08a86de19a878ea3607212edb", "shasum": "" }, "require": { @@ -2724,9 +2724,9 @@ ], "description": "Cloud Storage Client for PHP", "support": { - "source": "https://github.com/googleapis/google-cloud-php-storage/tree/v1.42.0" + "source": "https://github.com/googleapis/google-cloud-php-storage/tree/v1.42.1" }, - "time": "2024-05-19T17:27:42+00:00" + "time": "2024-07-08T23:14:13+00:00" }, { "name": "google/common-protos", @@ -6712,16 +6712,16 @@ }, { "name": "php-amqplib/php-amqplib", - "version": "v3.6.2", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/php-amqplib/php-amqplib.git", - "reference": "cb514530ce45a6d2f636be5196010c47c3bcf6e0" + "reference": "91fd00e74cd2eea624fd50a321d926b1c124bb99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/cb514530ce45a6d2f636be5196010c47c3bcf6e0", - "reference": "cb514530ce45a6d2f636be5196010c47c3bcf6e0", + "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/91fd00e74cd2eea624fd50a321d926b1c124bb99", + "reference": "91fd00e74cd2eea624fd50a321d926b1c124bb99", "shasum": "" }, "require": { @@ -6787,9 +6787,9 @@ ], "support": { "issues": "https://github.com/php-amqplib/php-amqplib/issues", - "source": "https://github.com/php-amqplib/php-amqplib/tree/v3.6.2" + "source": "https://github.com/php-amqplib/php-amqplib/tree/v3.7.0" }, - "time": "2024-04-15T18:31:22+00:00" + "time": "2024-07-09T21:10:28+00:00" }, { "name": "php-http/client-common", @@ -9731,6 +9731,65 @@ ], "time": "2023-12-25T11:46:58+00:00" }, + { + "name": "stripe/stripe-php", + "version": "v15.2.0", + "source": { + "type": "git", + "url": "https://github.com/stripe/stripe-php.git", + "reference": "29a28251d35dba236ad6e860e1927b3f35cc1b2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stripe/stripe-php/zipball/29a28251d35dba236ad6e860e1927b3f35cc1b2e", + "reference": "29a28251d35dba236ad6e860e1927b3f35cc1b2e", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": ">=5.6.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.5.0", + "phpstan/phpstan": "^1.2", + "phpunit/phpunit": "^5.7 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Stripe\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stripe and contributors", + "homepage": "https://github.com/stripe/stripe-php/contributors" + } + ], + "description": "Stripe PHP Library", + "homepage": "https://stripe.com/", + "keywords": [ + "api", + "payment processing", + "stripe" + ], + "support": { + "issues": "https://github.com/stripe/stripe-php/issues", + "source": "https://github.com/stripe/stripe-php/tree/v15.2.0" + }, + "time": "2024-07-11T18:46:58+00:00" + }, { "name": "symfony/cache", "version": "v7.1.2", diff --git a/database/migrations/Guild/2024_06_25_032857_peoples_subscriptions.php b/database/migrations/Guild/2024_06_25_032857_peoples_subscriptions.php new file mode 100644 index 000000000..e31967662 --- /dev/null +++ b/database/migrations/Guild/2024_06_25_032857_peoples_subscriptions.php @@ -0,0 +1,41 @@ +id(); + $table->bigInteger('apps_id')->unsigned()->index(); + $table->bigInteger('peoples_id')->unsigned()->index(); + $table->string('subscription_type')->index(); + $table->string('status')->index(); + $table->date('first_date'); + $table->date('start_date'); + $table->date('end_date')->nullable()->index(); + $table->date('next_renewal')->nullable()->index(); + $table->json('metadata')->nullable(); + $table->boolean('is_deleted')->default(0)->index(); + $table->dateTime('created_at')->index('created_at'); + $table->dateTime('updated_at')->nullable()->index('updated_at'); + + //index apps people + $table->index(['apps_id', 'peoples_id']); + $table->index(['peoples_id', 'subscription_type']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('peoples_subscriptions'); + } +}; diff --git a/src/Domains/Connectors/Ghost/Jobs/UpdatePeopleGhostSubscriptionJob.php b/src/Domains/Connectors/Ghost/Jobs/UpdatePeopleGhostSubscriptionJob.php new file mode 100644 index 000000000..984913e2b --- /dev/null +++ b/src/Domains/Connectors/Ghost/Jobs/UpdatePeopleGhostSubscriptionJob.php @@ -0,0 +1,42 @@ +webhookRequest->payload; + $app = $this->webhookRequest->receiverWebhook->app; + $company = $this->webhookRequest->receiverWebhook->company; + $people = PeoplesRepository::getByEmail($member['email'], $company); + if (! $people) { + throw new Exception('People not found'); + } + $dto = new PeopleSubscriptionDTO( + app: $app, + people: $people, + subscription_type: 'Free', + status: '1', + first_date: date('Y-m-d H:i:s', $member['created_at']), + start_date: date('Y-m-d H:i:s', $member['created_at']), + metadata: $this->webhookRequest->payload + ); + $action = new CreateOrUpdatePeopleSubscriptionAction($dto); + $peopleSub = $action->handle(); + + return [ + 'success' => true, + 'data' => $peopleSub, + ]; + } +} diff --git a/src/Domains/Connectors/Stripe/Enums/ConfigurationEnum.php b/src/Domains/Connectors/Stripe/Enums/ConfigurationEnum.php new file mode 100644 index 000000000..33e68de7f --- /dev/null +++ b/src/Domains/Connectors/Stripe/Enums/ConfigurationEnum.php @@ -0,0 +1,10 @@ +webhookRequest->payload['type'], ['customer.subscription.updated', 'customer.subscription.created', 'customer.subscription.deleted'])) { + Log::error('Webhook type not found', ['type' => $this->webhookRequest->payload['type']]); + + return []; + } + + $this->data = $this->webhookRequest->payload; + $webhookSub = $this->data['data']['object']; + $app = $this->webhookRequest->receiverWebhook->app; + $company = $this->webhookRequest->receiverWebhook->company; + $user = $this->webhookRequest->receiverWebhook->user; + + $stripe = new StripeClient($app->get(ConfigurationEnum::STRIPE_SECRET_KEY->value)); + $customer = $stripe->customers->retrieve( + $webhookSub['customer'], + ['expand' => ['subscriptions']] + ); + + if (! $customer->email) { + Log::error('Customer email not found'); + + return ['error' => 'Customer email not found ' . $customer->id]; + } + $people = PeoplesRepository::getByEmail($customer->email, $company); + if (! $people) { + Log::error('People not found'); + + return ['error' => 'People not found' . $customer->email]; + + return []; + } + $subscriptions = $customer->subscriptions->data[0]; + + $dto = new PeopleSubscriptionDTO( + app: $app, + people: $people, + subscription_type: $subscriptions['plan']['nickname'], + status: '1', + first_date: date('Y-m-d H:i:s', $subscriptions['created']), + start_date: date('Y-m-d H:i:s', $subscriptions['current_period_start']), + end_date: date('Y-m-d H:i:s', $subscriptions['ended_at']), + next_renewal: date('Y-m-d H:i:s', $subscriptions['current_period_end']), + metadata: $this->data ?? [], + ); + $action = new CreateOrUpdatePeopleSubscriptionAction($dto); + $peopleSub = $action->handle(); + + return [ + 'message' => 'People Subscription updated', + 'data' => $peopleSub, + ]; + } +} diff --git a/src/Domains/Guild/Customers/Actions/CreateOrUpdatePeopleSubscriptionAction.php b/src/Domains/Guild/Customers/Actions/CreateOrUpdatePeopleSubscriptionAction.php new file mode 100644 index 000000000..f937b0080 --- /dev/null +++ b/src/Domains/Guild/Customers/Actions/CreateOrUpdatePeopleSubscriptionAction.php @@ -0,0 +1,38 @@ + $this->peopleSubscriptionDTO->subscription_type, + 'status' => '1', + 'first_date' => $this->peopleSubscriptionDTO->first_date, + 'start_date' => $this->peopleSubscriptionDTO->start_date, + 'end_date' => $this->peopleSubscriptionDTO->end_date, + 'next_renewal' => $this->peopleSubscriptionDTO->next_renewal, + 'metadata' => $this->peopleSubscriptionDTO->metadata, + 'apps_id' => $this->peopleSubscriptionDTO->app->getId(), + ]; + + return PeopleSubscription::updateOrCreate( + $dataPeopleSub, + [ + 'peoples_id' => $this->peopleSubscriptionDTO->people->getId(), + 'subscription_type' => $this->peopleSubscriptionDTO->subscription_type, + ] + ); + } +} diff --git a/src/Domains/Guild/Customers/DataTransferObject/PeopleSubscription.php b/src/Domains/Guild/Customers/DataTransferObject/PeopleSubscription.php new file mode 100644 index 000000000..2cc66e3ac --- /dev/null +++ b/src/Domains/Guild/Customers/DataTransferObject/PeopleSubscription.php @@ -0,0 +1,25 @@ + ContactTypeEnum::EMAIL->value, + 'value' => fake()->email, + 'weight' => 1, + ]; + } +} diff --git a/src/Domains/Guild/Customers/Models/Contact.php b/src/Domains/Guild/Customers/Models/Contact.php index 4f45fff14..29d7ddc92 100644 --- a/src/Domains/Guild/Customers/Models/Contact.php +++ b/src/Domains/Guild/Customers/Models/Contact.php @@ -7,6 +7,7 @@ use Baka\Traits\NoAppRelationshipTrait; use Baka\Traits\NoCompanyRelationshipTrait; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Kanvas\Guild\Customers\Factories\ContactFactory; use Kanvas\Guild\Models\BaseModel; /** @@ -17,7 +18,6 @@ * @property int $peoples_id * @property string $value * @property int $weight - * */ class Contact extends BaseModel { @@ -44,4 +44,9 @@ public function type(): BelongsTo 'id' ); } + + protected static function newFactory() + { + return new ContactFactory(); + } } diff --git a/src/Domains/Guild/Customers/Models/PeopleSubscription.php b/src/Domains/Guild/Customers/Models/PeopleSubscription.php new file mode 100644 index 000000000..7b213801a --- /dev/null +++ b/src/Domains/Guild/Customers/Models/PeopleSubscription.php @@ -0,0 +1,29 @@ + Json::class, + ]; +} diff --git a/src/Domains/Workflow/Models/WorkflowAction.php b/src/Domains/Workflow/Models/WorkflowAction.php index c721f452d..82d4dc762 100644 --- a/src/Domains/Workflow/Models/WorkflowAction.php +++ b/src/Domains/Workflow/Models/WorkflowAction.php @@ -14,6 +14,8 @@ class WorkflowAction extends BaseModel protected $table = 'actions'; + protected $guarded = []; + protected static function newFactory(): Factory { return ActionFactory::new(); diff --git a/tests/Connectors/Integration/Ghosts/CreatePeopleSubscriptionTest.php b/tests/Connectors/Integration/Ghosts/CreatePeopleSubscriptionTest.php new file mode 100644 index 000000000..9c603b91d --- /dev/null +++ b/tests/Connectors/Integration/Ghosts/CreatePeopleSubscriptionTest.php @@ -0,0 +1,64 @@ +user(); + $company = $user->getCurrentCompany(); + + $people = People::factory() + ->withAppId($app->getId()) + ->withCompanyId($company->getId()) + ->has(Contact::factory()->count(1), 'contacts') + ->create(); + + $payload = [ + 'email' => $people->getEmails()[0]->value, + 'created_at' => time(), + 'current_period_start' => time(), + ]; + + $workflowAction = WorkflowAction::firstOrCreate([ + 'name' => 'Update People Subscription', + 'model_name' => UpdatePeopleGhostSubscriptionJob::class, + ]); + + $receiverWebhook = ReceiverWebhook::factory() + ->app($app->getId()) + ->user($user->getId()) + ->company($company->getId()) + ->create([ + 'action_id' => $workflowAction->getId(), + ]); + + $request = Request::create('https://localhost/ghosttest', 'POST', $payload); + + // Execute the action and get the webhook request + $webhookRequest = (new ProcessWebhookAttemptAction($receiverWebhook, $request))->execute(); + + // Fake the queue + Queue::fake(); + $job = new UpdatePeopleGhostSubscriptionJob($webhookRequest); + $result = $job->handle(); + $this->assertArrayHasKey('success', $result); + $this->assertArrayHasKey('data', $result); + $this->assertTrue($result['success']); + } +} diff --git a/tests/Connectors/Integration/Stripe/UpdateSubscriptionTest.php b/tests/Connectors/Integration/Stripe/UpdateSubscriptionTest.php new file mode 100644 index 000000000..db3f8cd8d --- /dev/null +++ b/tests/Connectors/Integration/Stripe/UpdateSubscriptionTest.php @@ -0,0 +1,101 @@ +user(); + $company = $user->getCurrentCompany(); + + $people = People::factory() + ->withAppId($app->getId()) + ->withCompanyId($company->getId()) + ->has(Contact::factory()->count(1), 'contacts') + ->create(); + + $app->set(ConfigurationEnum::STRIPE_SECRET_KEY->value, getenv('TEST_STRIPE_SECRET_KEY')); + $stripe = new StripeClient($app->get(ConfigurationEnum::STRIPE_SECRET_KEY->value)); + $customer = $stripe->customers->create([ + 'email' => $people->getEmails()[0]->value, + 'name' => $people->getName(), + ]); + $paymentMethod = $stripe->paymentMethods->create([ + 'type' => 'card', + 'card' => [ + 'number' => '4242424242424242', + 'exp_month' => 8, + 'exp_year' => 2026, + 'cvc' => '314', + ], + ]); + + $stripe->paymentMethods->attach( + $paymentMethod->id, + ['customer' => $customer->id] + ); + $stripe->customers->update( + $customer->id, + ['invoice_settings' => ['default_payment_method' => $paymentMethod->id]] + ); + + $prices = $stripe->prices->all(); + $stripe->subscriptions->create([ + 'customer' => $customer->id, + 'items' => [ + ['price' => $prices->data[0]->id], + ], + ]); + $payload = [ + 'type' => 'customer.subscription.updated', + 'data' => [ + 'object' => [ + 'customer' => $customer->id, + ], + ], + ]; + + $workflowAction = WorkflowAction::firstOrCreate([ + 'name' => 'Update People Subscription', + 'model_name' => UpdatePeopleStripeSubscriptionJob::class, + ]); + + $receiverWebhook = ReceiverWebhook::factory() + ->app($app->getId()) + ->user($user->getId()) + ->company($company->getId()) + ->create([ + 'action_id' => $workflowAction->getId(), + ]); + + $request = Request::create('https://localhost/shopifytest', 'POST', $payload); + + // Execute the action and get the webhook request + $webhookRequest = (new ProcessWebhookAttemptAction($receiverWebhook, $request))->execute(); + + // Fake the queue + Queue::fake(); + $job = new UpdatePeopleStripeSubscriptionJob($webhookRequest); + $result = $job->handle(); + + $this->assertArrayHasKey('message', $result); + $this->assertEquals('People Subscription updated', $result['message']); + } +}